From 2fde53de281e001be67230aefd2eb04c5a82f3ca Mon Sep 17 00:00:00 2001 From: awsp Date: Fri, 23 Jan 2015 15:03:54 -1000 Subject: [PATCH] init commit --- .DS_Store | Bin 0 -> 6148 bytes README.md | 66 +- awsp:handsontable-tests.js | 0 awsp:handsontable.js | 1 + bower_components/handsontable/.bower.json | 36 + bower_components/handsontable/CHANGELOG.md | 1 + bower_components/handsontable/CNAME | 1 + bower_components/handsontable/CONTRIBUTING.md | 16 + bower_components/handsontable/Gruntfile.js | 436 + bower_components/handsontable/LICENSE | 22 + bower_components/handsontable/README.md | 90 + bower_components/handsontable/bower.json | 22 + bower_components/handsontable/dist/README.md | 25 + .../handsontable/dist/handsontable.css | 1100 + .../handsontable/dist/handsontable.full.css | 1100 + .../handsontable/dist/handsontable.full.js | 21150 ++++++++++++++++ .../dist/handsontable.full.min.css | 10 + .../dist/handsontable.full.min.js | 26 + .../handsontable/dist/handsontable.js | 20377 +++++++++++++++ .../handsontable/handsontable.jquery.json | 37 + bower_components/handsontable/index.html | 416 + bower_components/handsontable/package.json | 28 + .../handsontable/sauce_connect.log | 26 + bower_components/handsontable/update.json | 10 + bower_components/zeroclipboard/.bower.json | 52 + bower_components/zeroclipboard/bower.json | 43 + bower_components/zeroclipboard/dist/.jshintrc | 70 + .../zeroclipboard/dist/ZeroClipboard.Core.js | 2017 ++ .../dist/ZeroClipboard.Core.min.js | 10 + .../dist/ZeroClipboard.Core.min.map | 1 + .../zeroclipboard/dist/ZeroClipboard.js | 2581 ++ .../zeroclipboard/dist/ZeroClipboard.min.js | 10 + .../zeroclipboard/dist/ZeroClipboard.min.map | 1 + .../zeroclipboard/dist/ZeroClipboard.swf | Bin 0 -> 6580 bytes index.html | 23 + lib/.DS_Store | Bin 0 -> 6148 bytes lib/handsontable.meteor.js | 21150 ++++++++++++++++ package.js | 25 + 38 files changed, 70977 insertions(+), 2 deletions(-) create mode 100644 .DS_Store create mode 100644 awsp:handsontable-tests.js create mode 100644 awsp:handsontable.js create mode 100644 bower_components/handsontable/.bower.json create mode 100644 bower_components/handsontable/CHANGELOG.md create mode 100644 bower_components/handsontable/CNAME create mode 100644 bower_components/handsontable/CONTRIBUTING.md create mode 100644 bower_components/handsontable/Gruntfile.js create mode 100644 bower_components/handsontable/LICENSE create mode 100644 bower_components/handsontable/README.md create mode 100644 bower_components/handsontable/bower.json create mode 100644 bower_components/handsontable/dist/README.md create mode 100644 bower_components/handsontable/dist/handsontable.css create mode 100644 bower_components/handsontable/dist/handsontable.full.css create mode 100644 bower_components/handsontable/dist/handsontable.full.js create mode 100644 bower_components/handsontable/dist/handsontable.full.min.css create mode 100644 bower_components/handsontable/dist/handsontable.full.min.js create mode 100644 bower_components/handsontable/dist/handsontable.js create mode 100644 bower_components/handsontable/handsontable.jquery.json create mode 100644 bower_components/handsontable/index.html create mode 100644 bower_components/handsontable/package.json create mode 100644 bower_components/handsontable/sauce_connect.log create mode 100644 bower_components/handsontable/update.json create mode 100644 bower_components/zeroclipboard/.bower.json create mode 100644 bower_components/zeroclipboard/bower.json create mode 100644 bower_components/zeroclipboard/dist/.jshintrc create mode 100644 bower_components/zeroclipboard/dist/ZeroClipboard.Core.js create mode 100644 bower_components/zeroclipboard/dist/ZeroClipboard.Core.min.js create mode 100644 bower_components/zeroclipboard/dist/ZeroClipboard.Core.min.map create mode 100644 bower_components/zeroclipboard/dist/ZeroClipboard.js create mode 100644 bower_components/zeroclipboard/dist/ZeroClipboard.min.js create mode 100644 bower_components/zeroclipboard/dist/ZeroClipboard.min.map create mode 100644 bower_components/zeroclipboard/dist/ZeroClipboard.swf create mode 100644 index.html create mode 100644 lib/.DS_Store create mode 100644 lib/handsontable.meteor.js create mode 100644 package.js diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..773bd5721a728acffc73f7f7d60146f9083c29e5 GIT binary patch literal 6148 zcmeHK%}T>S5Z-O8Ce07R=g^L$tNbtmb5`KM77Onk>}GJn`H*N>twaaid0r%Kx`7JAXJ8Vy1(Za3un zTFgtj9do1J_phL|?u34m#mr>^ORhCt*EPLk7Dl7;!G6)&Ep3mB)~He`fxlB3kIj*$ zZEo!yo}Bf@H@A29506jJFR$_pDBm`hH3<{@Hu2Cw29N<{V1XG>m!M`W@E&n%$N)0% zI|lguV1aV96{Zr!)d5C~0Dw6N3jv?c61>JzXe&%5!V?fGO95pmRU-zK7 z!j@ETo!cDmwHD|lC>NfWN}QB{BU&-!@>W~`6#{XKJAk&rR3bz`@JB$=2.1.6" + }, + "devDependencies": { + "chroma-js": "~0.5.6" + }, + "homepage": "https://github.com/handsontable/handsontable", + "_release": "0.12.4", + "_resolution": { + "type": "version", + "tag": "0.12.4", + "commit": "2cfb9798c790652e7f878ee92e7d7e0a1d7dd52e" + }, + "_source": "git://github.com/handsontable/handsontable.git", + "_target": "~0.12.4", + "_originalSource": "handsontable", + "_direct": true +} \ No newline at end of file diff --git a/bower_components/handsontable/CHANGELOG.md b/bower_components/handsontable/CHANGELOG.md new file mode 100644 index 0000000..dd28c1f --- /dev/null +++ b/bower_components/handsontable/CHANGELOG.md @@ -0,0 +1 @@ +All releases are described at https://github.com/handsontable/handsontable/releases diff --git a/bower_components/handsontable/CNAME b/bower_components/handsontable/CNAME new file mode 100644 index 0000000..8f001a3 --- /dev/null +++ b/bower_components/handsontable/CNAME @@ -0,0 +1 @@ +handsontable.com diff --git a/bower_components/handsontable/CONTRIBUTING.md b/bower_components/handsontable/CONTRIBUTING.md new file mode 100644 index 0000000..bed6e1d --- /dev/null +++ b/bower_components/handsontable/CONTRIBUTING.md @@ -0,0 +1,16 @@ +# Contributing to Handsontable + +Your contributions to the project are very welcome. If you would like to fix a bug or propose a new feature, you can submit a Pull Request. + +To help us merge your Pull Request, please make sure you follow these points: + +1. Please make your fix on a separate branch. This makes merging much easier. +2. Do not edit files `handsontable.js`, `handsontable.css`, `handsontable.full.js`, `handsontable.full.css`. Instead, try to edit files inside the `src/` directory and then use `grunt` to make a build. More information about this on wiki page [Building](https://github.com/handsontable/handsontable/wiki/Building). +3. Very important: For any change that you make, **please try to also add a test case(s)** in `tests/jasmine/spec/` or `src/3rdparty/walkontable/test/jasmine/spec/`. This helps us understand the issue and make sure that it will stay fixed forever. See [Testing](https://github.com/handsontable/handsontable/wiki/Testing) +4. Describe the problem in the Pull Request description (of course you would do it, why do I mention that?) + +Thank you for your commitment! + +## Team rules + +The Handsontable team utilizes Git-Flow. See [How we use Git-Flow](https://github.com/handsontable/handsontable/handsontable/wiki/How-we-use-Git-Flow) diff --git a/bower_components/handsontable/Gruntfile.js b/bower_components/handsontable/Gruntfile.js new file mode 100644 index 0000000..13c97cd --- /dev/null +++ b/bower_components/handsontable/Gruntfile.js @@ -0,0 +1,436 @@ +/** + * This file is used to build Handsontable from `src/*` + * + * Installation: + * 1. Install Grunt CLI (`npm install -g grunt-cli`) + * 1. Install Grunt 0.4.0 and other dependencies (`npm install`) + * + * Build: + * Execute `grunt` from root directory of this directory (where Gruntfile.js is) + * To execute automatically after each change, execute `grunt --force default watch` + * To execute build followed by the test run, execute `grunt test` + * + * Result: + * building Handsontable will create files: + * - dist/handsontable.js + * - dist/handsontable.css + * - dist/handsontable.full.js + * - dist/handsontable.full.css + * + * See http://gruntjs.com/getting-started for more information about Grunt + */ +var browsers = [ + { + browserName: 'firefox', + platform: 'Windows 7' + }, + { + browserName: 'chrome', + platform: 'Windows 7' + }, + { + browserName: 'opera', + platform: 'Windows 7' + }, + //{ + // browserName: 'internet explorer', + // version: '8', + // platform: 'Windows 7' + //}, + //{ + // browserName: 'internet explorer', + // version: '9', + // platform: 'Windows 7' + //}, + { + browserName: 'internet explorer', + version: '10', + platform: 'Windows 8' + } +]; + +module.exports = function (grunt) { + + require('time-grunt')(grunt); + + grunt.initConfig({ + pkg: grunt.file.readJSON('package.json'), + gitinfo: { + }, + meta: { + src: [ + 'tmp/core.js', + 'src/multiMap.js', + 'src/dom.js', + 'src/eventManager.js', + 'src/tableView.js', + 'src/editors.js', + 'src/editorManager.js', + 'src/renderers.js', + 'src/helpers.js', + 'src/dataMap.js', + + 'src/renderers/cellDecorator.js', + 'src/renderers/textRenderer.js', + 'src/renderers/autocompleteRenderer.js', + 'src/renderers/checkboxRenderer.js', + 'src/renderers/numericRenderer.js', + 'src/renderers/passwordRenderer.js', + 'src/renderers/htmlRenderer.js', + + 'src/editors/baseEditor.js', + 'src/editors/textEditor.js', + 'src/editors/mobileTextEditor.js', + 'src/editors/checkboxEditor.js', + 'src/editors/dateEditor.js', + 'src/editors/handsontableEditor.js', + 'src/editors/autocompleteEditor.js', + 'src/editors/passwordEditor.js', + 'src/editors/selectEditor.js', + 'src/editors/dropdownEditor.js', + 'src/editors/numericEditor.js', + + 'src/validators/numericValidator.js', + 'src/validators/autocompleteValidator.js', + + 'src/cellTypes.js', + + 'src/3rdparty/autoResize.js', + 'src/3rdparty/sheetclip.js', + 'src/3rdparty/copypaste.js', + 'src/3rdparty/json-patch-duplex.js', + + 'src/pluginHooks.js', + 'src/plugins/autoColumnSize.js', + 'src/plugins/columnSorting.js', + 'src/plugins/contextMenu.js', + 'src/plugins/comments.js', + 'src/plugins/legacy.js', + 'src/plugins/manualColumnMove.js', + 'src/plugins/manualColumnResize.js', + 'src/plugins/manualRowResize.js', + 'src/plugins/observeChanges.js', + 'src/plugins/persistentState.js', + 'src/plugins/undoRedo.js', + 'src/plugins/dragToScroll/dragToScroll.js', + 'src/plugins/copyPaste.js', + 'src/plugins/search.js', + 'src/plugins/mergeCells/mergeCells.js', + 'src/plugins/customBorders/customBorders.js', + 'src/plugins/manualRowMove.js', + 'src/plugins/autofill.js', + 'src/plugins/grouping/grouping.js', + 'src/plugins/contextMenuCopyPaste/contextMenuCopyPaste.js', + 'src/plugins/multipleSelectionHandles/multipleSelectionHandles.js', + 'src/plugins/touchScroll/touchScroll.js', + 'src/plugins/manualColumnFreeze/manualColumnFreeze.js' + ], + walkontable: [ + 'src/3rdparty/walkontable/src/*.js', + 'src/3rdparty/walkontable/src/3rdparty/*.js' + ], + vendor: [ + 'lib/numeral.js' + ], + shims: [ + 'lib/shims/array.indexOf.js', + 'lib/shims/array.filter.js', + 'lib/shims/array.isArray.js', + 'lib/shims/object.keys.js', + 'lib/shims/weakmap.js' + ] + }, + + concat: { + dist: { + files: { + 'dist/handsontable.js': [ + 'tmp/intro.js', + '<%= meta.shims %>', + '<%= meta.src %>', + '<%= meta.walkontable %>', + 'plugins/jqueryHandsontable.js', + 'src/outro.js' + ], + 'dist/handsontable.css': [ + 'tmp/handsontable.css', + 'src/css/mobile.handsontable.css' + ] + } + }, + full_js: { + files: { + 'dist/handsontable.full.js': [ + 'dist/handsontable.js', + '<%= meta.vendor %>' + ] + } + }, + full_css: { + files: { + 'dist/handsontable.full.css': [ + 'dist/handsontable.css' + ] + } + } + }, + + watch: { + options: { + livereload: true //works with Chrome LiveReload extension. See: https://github.com/gruntjs/grunt-contrib-watch + }, + files: [ + 'src/**/*.js', + 'src/**/*.css', + 'src/**/*.html', + '!src/3rdparty/walkontable/test/**/*', + 'lib/**/*.js', + 'lib/**/*.css' + ], + tasks: ['default'] + }, + + clean: { + dist: ['tmp'] + }, + + replace: { + dist: { + options: { + variables: { + version: '<%= pkg.version %>', + timestamp: '<%= (new Date()).toString() %>' + } + }, + files: { + 'tmp/intro.js': 'src/intro.js', + 'tmp/core.js': 'src/core.js', + 'tmp/handsontable.css': 'src/css/handsontable.css' + } + } + }, + jasmine: { + handsontable: { + src: [ + 'dist/handsontable.js', + 'demo/js/backbone/lodash.underscore.js', + 'demo/js/backbone/backbone.js', + 'demo/js/backbone/backbone-relational/backbone-relational.js', + 'lib/jquery-ui/js/jquery-ui.custom.js', + 'plugins/removeRow/handsontable.removeRow.js' + ], + options: { + specs: [ + 'test/jasmine/spec/*Spec.js', + 'test/jasmine/spec/!(mobile)*/*Spec.js', + 'src/plugins/*/test/*.spec.js', + 'plugins/*/test/*.spec.js', + 'test/jasmine/spec/MemoryLeakTest.js' + ], + styles: [ + 'test/jasmine/css/SpecRunner.css', + 'dist/handsontable.css', + 'plugins/removeRow/handsontable.removeRow.css', + 'lib/jquery-ui/css/ui-bootstrap/jquery-ui.custom.css' + ], + vendor: [ + 'lib/jquery.min.js', + 'lib/numeral.js', + 'lib/numeral.de-de.js', + 'test/jasmine/lib/jasmine-extensions.js' + ], + helpers: [ + 'test/jasmine/spec/SpecHelper.js', + 'test/jasmine/lib/nodeShim.js', + 'test/jasmine/spec/test-init.js' + ], + outfile: 'test/jasmine/SpecRunner.html', + template: 'test/jasmine/templates/SpecRunner.tmpl', + keepRunner: true + } + }, + walkontable: { + src: [ + 'src/dom.js', + 'src/helpers.js', + 'src/eventManager.js', + 'src/3rdparty/walkontable/src/*.js', + 'src/3rdparty/walkontable/src/3rdparty/*.js' + ], + options: { + specs: [ + 'src/3rdparty/walkontable/test/jasmine/spec/*.spec.js' + ], + styles: [ + 'src/3rdparty/walkontable/css/walkontable.css' + ], + vendor: [ + 'lib/jquery.min.js' + ], + helpers: [ + 'src/3rdparty/walkontable/test/jasmine/SpecHelper.js', + 'test/jasmine/lib/nodeShim.js', + 'src/3rdparty/walkontable/test/jasmine/test-init.js' + + ], + outfile: 'src/3rdparty/walkontable/test/jasmine/SpecRunner.html', + template: 'test/jasmine/templates/SpecRunner.tmpl', + keepRunner: true + } + }, + mobile: { + src: [ + 'dist/handsontable.js', + 'demo/js/backbone/lodash.underscore.js', + 'demo/js/backbone/backbone.js', + 'demo/js/backbone/backbone-relational/backbone-relational.js', + 'lib/jquery-ui/js/jquery-ui.custom.js', + 'plugins/removeRow/handsontable.removeRow.js' + ], + options: { + specs: [ + 'test/jasmine/spec/mobile/*Spec.js', + 'src/plugins/*/test/mobile/*.spec.js' + ], + styles: [ + 'test/jasmine/css/SpecRunner.css', + 'dist/handsontable.css', + 'plugins/removeRow/handsontable.removeRow.css', + 'lib/jquery-ui/css/ui-bootstrap/jquery-ui.custom.css' + ], + vendor: [ + 'lib/jquery.min.js', + 'lib/numeral.js', + 'lib/numeral.de-de.js', + 'test/jasmine/lib/jasmine-extensions.js' + ], + helpers: [ + 'test/jasmine/spec/SpecHelper.js', + 'test/jasmine/spec/MobileSpecHelper.js', + 'test/jasmine/lib/nodeShim.js', + 'test/jasmine/spec/test-init.js' + ], + outfile: 'test/jasmine/MobileSpecRunner.html', + template: 'test/jasmine/templates/SpecRunner.tmpl', + keepRunner: true + } + } + }, + uglify: { + options: { + preserveComments: 'some' + }, + "dist/handsontable.full.min.js": ["dist/handsontable.full.js" ] + }, + cssmin: { + minify: { + expand: true, + cwd: 'dist/', + src: ['handsontable.full.css'], + dest: 'dist/', + extDot: 'last', + ext: '.min.css' + } + }, + connect: { + server: { + options: { + port: 8080, + base: '.', + keepalive: true + } + }, + sauce: { + options: { + port: 9999, + base: '.', + keepalive: false + } + } + }, + 'saucelabs-jasmine': { + handsontable: { + options: { + urls: ['http://localhost:9999/test/jasmine/SpecRunner.html'], +// build: process.env.TRAVIS_JOB_ID, + build: '<%= pkg.version %>-<%= gitinfo.local.branch.current.name %>', + concurrency: 3, + browsers: browsers, + testname: "Development test (Handsontable)" + } + }, + walkontable: { + options: { + urls: ['http://localhost:9999/src/3rdparty/walkontable/test/jasmine/SpecRunner.html'], +// build: process.env.TRAVIS_JOB_ID, + build: '<%= pkg.version %>-<%= gitinfo.local.branch.current.name %>', + concurrency: 3, + browsers: browsers, + testname: "Development test (Walkontable)" + } + } + }, + jshint: (function() { + var options = { + options: { + jshintrc: true + } + }; + options.core = 'src/core.js'; + options.src = '<%= meta.src %>'; + options.walkontable = '<%= meta.walkontable %>'; + + return options; + }()) + }); + + // Default task. + grunt.registerTask('default', ['jshint', 'gitinfo', 'replace:dist', 'concat', 'uglify', 'cssmin', 'clean']); + grunt.registerTask('test', ['default', 'jasmine:handsontable', 'jasmine:walkontable', 'jasmine:mobile:build']); + grunt.registerTask('test:handsontable', ['default', 'jasmine:handsontable']); + grunt.registerTask('test:walkontable', ['default', 'jasmine:walkontable']); + grunt.registerTask('test:mobile', ['default', 'jasmine:mobile:build']); + grunt.registerTask('sauce', ['default', 'connect:sauce', 'saucelabs-jasmine:walkontable', 'saucelabs-jasmine:handsontable']); + grunt.registerTask('sauce:handsontable', ['default', 'connect:sauce', 'saucelabs-jasmine:handsontable']); + grunt.registerTask('sauce:walkontable', ['default', 'connect:sauce', 'saucelabs-jasmine:walkontable']); + + + grunt.registerTask('singletest', 'Runs all tests from a single Spec file.\nSyntax: grunt singletest:[handsontable, walkontable]:', function (taskName, specFile) { + var context = { + taskName: taskName, + specFile: specFile + }; + + var configProperty = grunt.template.process('jasmine.<%=taskName%>.options.specs', {data: context}); + var task = grunt.template.process('jasmine:<%=taskName%>', {data: context}); + var specPath; + + switch (taskName) { + case 'handsontable': + specPath = grunt.template.process('test/jasmine/spec/<%=specFile%>', {data: context}); + break; + case 'walkontable': + specPath = grunt.template.process('src/3rdparty/walkontable/test/jasmine/spec/<%=specFile%>', {data: context}); + break; + default: + grunt.fail.fatal('Unknown test task: "' + taskName + '". Available test tasks: [handsontable, walkontable]') + } + + grunt.config.set(configProperty, [specPath]); + + grunt.task.run(task); + }); + + + grunt.loadNpmTasks('grunt-replace'); + grunt.loadNpmTasks('grunt-contrib-clean'); + grunt.loadNpmTasks('grunt-contrib-concat'); + grunt.loadNpmTasks('grunt-contrib-watch'); + grunt.loadNpmTasks('grunt-contrib-jasmine'); + grunt.loadNpmTasks('grunt-contrib-connect'); + grunt.loadNpmTasks('grunt-contrib-uglify'); + grunt.loadNpmTasks('grunt-contrib-cssmin'); + grunt.loadNpmTasks('grunt-saucelabs'); + grunt.loadNpmTasks('grunt-gitinfo'); + grunt.loadNpmTasks('grunt-contrib-jshint'); +}; diff --git a/bower_components/handsontable/LICENSE b/bower_components/handsontable/LICENSE new file mode 100644 index 0000000..cb963d5 --- /dev/null +++ b/bower_components/handsontable/LICENSE @@ -0,0 +1,22 @@ +(The MIT License) + +Copyright (c) 2012-2014 Marcin Warpechowski + +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. \ No newline at end of file diff --git a/bower_components/handsontable/README.md b/bower_components/handsontable/README.md new file mode 100644 index 0000000..f0b6390 --- /dev/null +++ b/bower_components/handsontable/README.md @@ -0,0 +1,90 @@ +# Handsontable [![Build Status](https://travis-ci.org/handsontable/handsontable.png?branch=master)](https://travis-ci.org/handsontable/handsontable) + +Handsontable is a minimalist approach to Excel-like table editor (datagrid/data grid) in HTML & JavaScript. + +Runs in IE 10+, Firefox, Chrome, Safari and Opera. + +See the demos at http://handsontable.com/ or fork the example on [JSFiddle](http://jsfiddle.net/warpech/hU6Kz/). + +## Usage + +First, include all the dependencies. All the files that you need are in the `dist\` directory: + +```html + + +``` + +Then, create a new `Handsontable` object, passing a reference to an empty div as a first argument. After that, load some data if you wish: + +```html +
+ +``` + +## API Reference + +Check out the new wiki pages: [Options](https://github.com/handsontable/handsontable/wiki/Options), [Methods](https://github.com/handsontable/handsontable/wiki/Methods) and [Events](https://github.com/handsontable/handsontable/wiki/Events) + +## Changelog + +To see the list of recent changes, see [Releases](https://github.com/handsontable/handsontable/releases). + +## Questions + +Please use the :new: [Handsontable Google Group](https://groups.google.com/forum/?fromgroups=#!forum/handsontable) for posting general **Questions**. + +Make sure the question was not answered before in [FAQ](https://github.com/handsontable/handsontable/wiki/FAQ) or [GitHub Issues](https://github.com/handsontable/handsontable/issues) + +## Reporting bugs and feature requests + +Please follow this guidelines when reporting bugs and feature requests: + +1. Use [GitHub Issues](https://github.com/handsontable/handsontable/issues) board to report bugs and feature requests (not my email address) +2. Please **always** write steps to reporoduce the error. That way we can focus on fixing the bug, not scratching our heads trying to reproduce it. +3. If possible, please add a JSFiddle link that shows the problem (start by forking [this fiddle](http://jsfiddle.net/warpech/hU6Kz/)). It saves me much time. +4. If you can't reproduce it on JSFiddle, please add a screenshot that shows the problem. JSFiddle is much more appreciated because it lets me start fixing straight away. + +Thanks for understanding! + +## Contributing + +Please see [CONTRIBUTING.md](CONTRIBUTING.md) + +## Similar projects + +I want to stay motivated to keep Handsontable the best possible editable datagrid on the Web. Therefore, I invite you to check out alternative projects. I would love to receive feedback if you would like to import some of their features to Handsontable. + + - [DataTables](http://datatables.net/) + - [SlickGrid](https://github.com/mleibman/SlickGrid) + - [jqGrid](http://www.trirand.com/blog/) + - [jTable](http://www.jtable.org/) + - [jui_datagrid](http://www.pontikis.net/labs/jui_datagrid/) + - [ParamQuery](http://paramquery.com/) + - [Ember Table](http://addepar.github.io/ember-table/) + - [Backgrid.js](http://backgridjs.com/) + - [dgrid](http://dojofoundation.org/packages/dgrid/) + +## Contact + +You can contact us at hello@handsontable.com. + +## License + +The MIT License (see the [LICENSE](https://github.com/handsontable/handsontable/blob/master/LICENSE) file for the full text) diff --git a/bower_components/handsontable/bower.json b/bower_components/handsontable/bower.json new file mode 100644 index 0000000..2cfb638 --- /dev/null +++ b/bower_components/handsontable/bower.json @@ -0,0 +1,22 @@ +{ + "name": "handsontable", + "description": "Spreadsheet-like data grid editor that provides copy/paste functionality compatible with Excel/Google Docs", + "version": "0.12.4", + "main": ["./dist/handsontable.full.js", "./dist/handsontable.full.css"], + "ignore": [ + "**/.*", + "components", + "demo", + "lib", + "plugins", + "node_modules", + "src", + "test" + ], + "dependencies": { + "zeroclipboard": ">=2.1.6" + }, + "devDependencies": { + "chroma-js": "~0.5.6" + } +} diff --git a/bower_components/handsontable/dist/README.md b/bower_components/handsontable/dist/README.md new file mode 100644 index 0000000..818e3dc --- /dev/null +++ b/bower_components/handsontable/dist/README.md @@ -0,0 +1,25 @@ +# Handsontable distributions + +## Full distribution (recommended) + +The full distribution allows you to use Handsontable by just including 2 files: +```html + + +``` +(It may also require jQuery, if you're using the Datepicker for date input) + +**handsontable.full.js** and **handsontable.full.css** are compiled with ___all___ the needed dependencies. + +Using this has the same effect as loading all the dependencies from the Bare distribution (see below). + +## Bare distribution + +If you are a "Bob the Builder" kind of hacker, you will need to load Handsontable JS, CSS and their dependecies: +```html + + + +``` + +**handsontable.js** and **handsontable.css** are compiled ___without___ the needed dependencies. diff --git a/bower_components/handsontable/dist/handsontable.css b/bower_components/handsontable/dist/handsontable.css new file mode 100644 index 0000000..306f1e0 --- /dev/null +++ b/bower_components/handsontable/dist/handsontable.css @@ -0,0 +1,1100 @@ +/*! + * Handsontable 0.12.4 + * Handsontable is a JavaScript library 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: Fri Jan 23 2015 10:07:24 GMT+0100 (CET) + */ + +.handsontable { + position: relative; +} + +.handsontable .hide{ + display: none; +} +.handsontable .relative { + position: relative; +} + +.handsontable.htAutoColumnSize { + visibility: hidden; + left: 0; + position: absolute; + top: 0; +} + +.handsontable .wtHider { + width: 0; +} + +.handsontable .wtSpreader { + position: relative; + width: 0; /*must be 0, otherwise blank space appears in scroll demo after scrolling max to the right */ + height: auto; +} + +.handsontable table, +.handsontable tbody, +.handsontable thead, +.handsontable td, +.handsontable th, +.handsontable input, +.handsontable textarea, +.handsontable div { + box-sizing: content-box; + -webkit-box-sizing: content-box; + -moz-box-sizing: content-box; +} + +.handsontable input, +.handsontable textarea { + min-height: initial; +} + +.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*/ + /*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/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 { + background-color: #ff4c42 !important; /*gives priority over td.area selection background*/ +} + +.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: inherit; + line-height: inherit; + 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*/ +} + +.handsontableInput:focus { + border: 2px solid #5292F7; +} + +.handsontableInputHolder { + position: absolute; + top: 0; + left: 0; + z-index: 100; +} + +.htSelectEditor { + -webkit-appearance: menulist-button !important; + position: absolute; + width: auto; +} + +/* +TextRenderer readOnly cell +*/ + +.handsontable .htDimmed { + color: #777; +} + +.handsontable .htSubmenu { + position: relative; +} + +.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; +} + +@-webkit-keyframes opacity-hide { + from { + opacity: 1; + } + to { + opacity: 0; + /*display: none;*/ + } +} +@keyframes opacity-hide { + from { + /*display: block;*/ + opacity: 1; + } + to { + opacity: 0; + /*display: none;*/ + } +} + +@-webkit-keyframes opacity-show { + from { + opacity: 0; + /*display: none;*/ + } + to { + opacity: 1; + /*display: block;*/ + } +} +@keyframes opacity-show { + from { + opacity: 0; + /*display: none;*/ + } + to { + opacity: 1; + /*display: block;*/ + } +} + +/** + * 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: 17px; +} +.handsontable .autocompleteEditor.handsontable.htMacScroll { + 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: 1060; /*needs to be higher than 1050 - z-index for Twitter Bootstrap modal (#1569)*/ +} + +.htContextMenu .ht_clone_top, +.htContextMenu .ht_clone_left, +.htContextMenu .ht_clone_corner, +.htContextMenu .ht_clone_debug { + display: none; +} + +.ht_clone_top { + z-index: 101; +} + +.ht_clone_left { + z-index: 102; +} + +.ht_clone_corner { + z-index: 103; +} + +.ht_clone_debug { + 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, +.htContextMenu table tbody tr td.zeroclipboard-is-hover { + 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; + +} + +/* Grouping indicators */ +.handsontable colgroup col.rowHeader.htGroupCol { + width: 25px !important; +} +.handsontable colgroup col.rowHeader.htGroupColClosest { + width: 30px !important; +} + +.handsontable .htGroupIndicatorContainer { + background: #fff; + border: 0px; + padding-bottom: 0px; + vertical-align: bottom; + position: relative; +} + +.handsontable thead .htGroupIndicatorContainer { + vertical-align: top; + border-bottom: 0px; +} + +.handsontable tbody tr th:nth-last-child(2) { + border-right: 1px solid #CCC; +} + +.handsontable thead tr:nth-last-child(2) th { + border-bottom: 1px solid #CCC; + padding-bottom: 5px; +} + + +.ht_clone_corner thead tr th:nth-last-child(2) { + border-right: 1px solid #CCC; +} + +.htVerticalGroup { + height: 100%; +} + +.htHorizontalGroup { + width: 100%; + height: 100%; +} + +.htVerticalGroup:not(.htCollapseButton):after { + content: ""; + height: 100%; + width: 1px; + display: block; + background: #ccc; + margin-left: 5px; +} + +.htHorizontalGroup:not(.htCollapseButton):after { + content: ""; + width: 100%; + height: 1px; + display: block; + background: #ccc; + margin-top: 20%; +} + +.htCollapseButton { + width: 10px; + height: 10px; + line-height: 10px; + text-align: center; + border-radius: 5px; + border: 1px solid #f3f3f3; + -webkit-box-shadow: 1px 1px 3px rgba(0, 0, 0, 0.4); + box-shadow: 1px 1px 3px rgba(0, 0, 0, 0.4); + cursor: pointer; + margin-bottom: 3px; + position: relative; +} + +.htCollapseButton:after { + content: ""; + height: 300%; + width: 1px; + display: block; + background: #ccc; + margin-left: 4px; + position: absolute; + /*top: -300%;*/ + bottom: 10px; +} + + +thead .htCollapseButton { + right: 5px; + position: absolute; + top: 5px; + background: #fff; +} + +thead .htCollapseButton:after { + height: 1px; + width: 700%; + right: 10px; + top: 4px; +} + +.handsontable tr th .htGroupStart:after { + background: transparent; + border-left: 1px solid #ccc; + border-top: 1px solid #ccc; + width: 5px; + position: relative; + top: 50%; +} + +.handsontable thead tr th .htGroupStart:after { + background: transparent; + border-left: 1px solid #ccc; + border-top: 1px solid #ccc; + height: 5px; + width: 50%; + position: relative; + top: 0px; + left: 50%; +} + +.handsontable .htGroupLevelTrigger { + -webkit-box-shadow: 1px 1px 3px rgba(0, 0, 0, 0.4); + box-shadow: 1px 1px 3px rgba(0, 0, 0, 0.4); + width: 15px; + height: 15px; + margin: 4px auto; + padding: 0px; + line-height: 15px; + cursor: pointer; +} + +.handsontable tr th .htExpandButton { + position: absolute; + width: 10px; + height: 10px; + line-height: 10px; + text-align: center; + border-radius: 5px; + border: 1px solid #f3f3f3; + -webkit-box-shadow: 1px 1px 3px rgba(0, 0, 0, 0.4); + box-shadow: 1px 1px 3px rgba(0, 0, 0, 0.4); + cursor: pointer; + top: 0px; + display: none; +} + +.handsontable thead tr th .htExpandButton { + /*left: 5px;*/ + top: 5px; +} + +.handsontable tr th .htExpandButton.clickable { + display: block; +} + +.handsontable col.hidden { + width: 0px !important; +} + +.handsontable tr.hidden, +.handsontable tr.hidden td, +.handsontable tr.hidden th { + display: none; +} + +/*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; + } +} + +/* + + Handsontable Mobile Text Editor stylesheet + + */ + +.handsontable.mobile { + -webkit-touch-callout:none; + -webkit-user-select:none; + -khtml-user-select:none; + -moz-user-select:none; + -ms-user-select:none; + user-select:none; + -webkit-tap-highlight-color:rgba(0,0,0,0); + -webkit-overflow-scrolling: touch; +} + +.htMobileEditorContainer { + display: none; + position: absolute; + top: 0; + width: 70%; + height: 54pt; + background: #f8f8f8; + border-radius: 20px; + border: 1px solid #ebebeb; + z-index: 999; + box-sizing: border-box; + -webkit-box-sizing: border-box; + -webkit-text-size-adjust: none; +} + +.topLeftSelectionHandle:not(.ht_master .topLeftSelectionHandle), +.topLeftSelectionHandle-HitArea:not(.ht_master .topLeftSelectionHandle-HitArea) { + z-index: 9999; +} + +/* Initial left/top coordinates - overwritten when actual position is set */ +.topLeftSelectionHandle, +.topLeftSelectionHandle-HitArea, +.bottomRightSelectionHandle, +.bottomRightSelectionHandle-HitArea { + left: -10000px; + top: -10000px; +} + +.htMobileEditorContainer.active { + display: block; +} + +.htMobileEditorContainer .inputs { + position: absolute; + right: 210pt; + bottom: 10pt; + top: 10pt; + left: 14px; + height: 34pt; +} + +.htMobileEditorContainer .inputs textarea { + font-size: 13pt; + border: 1px solid #a1a1a1; + -webkit-appearance: none; + -webkit-box-shadow: none; + -moz-box-shadow: none; + box-shadow: none; + position: absolute; + left: 14px; + right: 14px; + top: 0; + bottom: 0; + padding: 7pt; +} + +.htMobileEditorContainer .cellPointer { + position: absolute; + top: -13pt; + height: 0; + width: 0; + left: 30px; + + border-left: 13pt solid transparent; + border-right: 13pt solid transparent; + border-bottom: 13pt solid #ebebeb; +} + +.htMobileEditorContainer .cellPointer.hidden { + display: none; +} + +.htMobileEditorContainer .cellPointer:before { + content: ''; + display: block; + position: absolute; + top: 2px; + height: 0; + width: 0; + left: -13pt; + + border-left: 13pt solid transparent; + border-right: 13pt solid transparent; + border-bottom: 13pt solid #f8f8f8; +} + +.htMobileEditorContainer .moveHandle { + position: absolute; + top: 10pt; + left: 5px; + width: 30px; + bottom: 0px; + cursor: move; + z-index: 9999; +} + +.htMobileEditorContainer .moveHandle:after { + content: "..\a..\a..\a.."; + white-space: pre; + line-height: 10px; + font-size: 20pt; + display: inline-block; + margin-top: -8px; + color: #ebebeb; +} + +.htMobileEditorContainer .positionControls { + width: 205pt; + position: absolute; + right: 5pt; + top: 0; + bottom: 0; +} + +.htMobileEditorContainer .positionControls > div { + width: 50pt; + height: 100%; + float: left; +} + +.htMobileEditorContainer .positionControls > div:after { + content: " "; + display: block; + width: 15pt; + height: 15pt; + text-align: center; + line-height: 50pt; +} + +.htMobileEditorContainer .leftButton:after, +.htMobileEditorContainer .rightButton:after, +.htMobileEditorContainer .upButton:after, +.htMobileEditorContainer .downButton:after { + transform-origin: 5pt 5pt; + -webkit-transform-origin: 5pt 5pt; + margin: 21pt 0 0 21pt; +} + +.htMobileEditorContainer .leftButton:after { + border-top: 2px solid #288ffe; + border-left: 2px solid #288ffe; + -webkit-transform: rotate(-45deg); + /*margin-top: 17pt;*/ + /*margin-left: 20pt;*/ +} +.htMobileEditorContainer .leftButton:active:after { + border-color: #cfcfcf; +} + +.htMobileEditorContainer .rightButton:after { + border-top: 2px solid #288ffe; + border-left: 2px solid #288ffe; + -webkit-transform: rotate(135deg); + /*margin-top: 17pt;*/ + /*margin-left: 10pt;*/ +} +.htMobileEditorContainer .rightButton:active:after { + border-color: #cfcfcf; +} + +.htMobileEditorContainer .upButton:after { + /*border-top: 2px solid #cfcfcf;*/ + border-top: 2px solid #288ffe; + border-left: 2px solid #288ffe; + -webkit-transform: rotate(45deg); + /*margin-top: 22pt;*/ + /*margin-left: 15pt;*/ +} +.htMobileEditorContainer .upButton:active:after { + border-color: #cfcfcf; +} + +.htMobileEditorContainer .downButton:after { + border-top: 2px solid #288ffe; + border-left: 2px solid #288ffe; + -webkit-transform: rotate(225deg); + /*margin-top: 15pt;*/ + /*margin-left: 15pt;*/ +} +.htMobileEditorContainer .downButton:active:after { + border-color: #cfcfcf; +} + +.handsontable.hide-tween { + -webkit-animation: opacity-hide 0.3s; + animation: opacity-hide 0.3s; + animation-fill-mode: forwards; + -webkit-animation-fill-mode: forwards; +} + +.handsontable.show-tween { + -webkit-animation: opacity-show 0.3s; + animation: opacity-show 0.3s; + animation-fill-mode: forwards; + -webkit-animation-fill-mode: forwards; +} diff --git a/bower_components/handsontable/dist/handsontable.full.css b/bower_components/handsontable/dist/handsontable.full.css new file mode 100644 index 0000000..306f1e0 --- /dev/null +++ b/bower_components/handsontable/dist/handsontable.full.css @@ -0,0 +1,1100 @@ +/*! + * Handsontable 0.12.4 + * Handsontable is a JavaScript library 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: Fri Jan 23 2015 10:07:24 GMT+0100 (CET) + */ + +.handsontable { + position: relative; +} + +.handsontable .hide{ + display: none; +} +.handsontable .relative { + position: relative; +} + +.handsontable.htAutoColumnSize { + visibility: hidden; + left: 0; + position: absolute; + top: 0; +} + +.handsontable .wtHider { + width: 0; +} + +.handsontable .wtSpreader { + position: relative; + width: 0; /*must be 0, otherwise blank space appears in scroll demo after scrolling max to the right */ + height: auto; +} + +.handsontable table, +.handsontable tbody, +.handsontable thead, +.handsontable td, +.handsontable th, +.handsontable input, +.handsontable textarea, +.handsontable div { + box-sizing: content-box; + -webkit-box-sizing: content-box; + -moz-box-sizing: content-box; +} + +.handsontable input, +.handsontable textarea { + min-height: initial; +} + +.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*/ + /*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/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 { + background-color: #ff4c42 !important; /*gives priority over td.area selection background*/ +} + +.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: inherit; + line-height: inherit; + 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*/ +} + +.handsontableInput:focus { + border: 2px solid #5292F7; +} + +.handsontableInputHolder { + position: absolute; + top: 0; + left: 0; + z-index: 100; +} + +.htSelectEditor { + -webkit-appearance: menulist-button !important; + position: absolute; + width: auto; +} + +/* +TextRenderer readOnly cell +*/ + +.handsontable .htDimmed { + color: #777; +} + +.handsontable .htSubmenu { + position: relative; +} + +.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; +} + +@-webkit-keyframes opacity-hide { + from { + opacity: 1; + } + to { + opacity: 0; + /*display: none;*/ + } +} +@keyframes opacity-hide { + from { + /*display: block;*/ + opacity: 1; + } + to { + opacity: 0; + /*display: none;*/ + } +} + +@-webkit-keyframes opacity-show { + from { + opacity: 0; + /*display: none;*/ + } + to { + opacity: 1; + /*display: block;*/ + } +} +@keyframes opacity-show { + from { + opacity: 0; + /*display: none;*/ + } + to { + opacity: 1; + /*display: block;*/ + } +} + +/** + * 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: 17px; +} +.handsontable .autocompleteEditor.handsontable.htMacScroll { + 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: 1060; /*needs to be higher than 1050 - z-index for Twitter Bootstrap modal (#1569)*/ +} + +.htContextMenu .ht_clone_top, +.htContextMenu .ht_clone_left, +.htContextMenu .ht_clone_corner, +.htContextMenu .ht_clone_debug { + display: none; +} + +.ht_clone_top { + z-index: 101; +} + +.ht_clone_left { + z-index: 102; +} + +.ht_clone_corner { + z-index: 103; +} + +.ht_clone_debug { + 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, +.htContextMenu table tbody tr td.zeroclipboard-is-hover { + 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; + +} + +/* Grouping indicators */ +.handsontable colgroup col.rowHeader.htGroupCol { + width: 25px !important; +} +.handsontable colgroup col.rowHeader.htGroupColClosest { + width: 30px !important; +} + +.handsontable .htGroupIndicatorContainer { + background: #fff; + border: 0px; + padding-bottom: 0px; + vertical-align: bottom; + position: relative; +} + +.handsontable thead .htGroupIndicatorContainer { + vertical-align: top; + border-bottom: 0px; +} + +.handsontable tbody tr th:nth-last-child(2) { + border-right: 1px solid #CCC; +} + +.handsontable thead tr:nth-last-child(2) th { + border-bottom: 1px solid #CCC; + padding-bottom: 5px; +} + + +.ht_clone_corner thead tr th:nth-last-child(2) { + border-right: 1px solid #CCC; +} + +.htVerticalGroup { + height: 100%; +} + +.htHorizontalGroup { + width: 100%; + height: 100%; +} + +.htVerticalGroup:not(.htCollapseButton):after { + content: ""; + height: 100%; + width: 1px; + display: block; + background: #ccc; + margin-left: 5px; +} + +.htHorizontalGroup:not(.htCollapseButton):after { + content: ""; + width: 100%; + height: 1px; + display: block; + background: #ccc; + margin-top: 20%; +} + +.htCollapseButton { + width: 10px; + height: 10px; + line-height: 10px; + text-align: center; + border-radius: 5px; + border: 1px solid #f3f3f3; + -webkit-box-shadow: 1px 1px 3px rgba(0, 0, 0, 0.4); + box-shadow: 1px 1px 3px rgba(0, 0, 0, 0.4); + cursor: pointer; + margin-bottom: 3px; + position: relative; +} + +.htCollapseButton:after { + content: ""; + height: 300%; + width: 1px; + display: block; + background: #ccc; + margin-left: 4px; + position: absolute; + /*top: -300%;*/ + bottom: 10px; +} + + +thead .htCollapseButton { + right: 5px; + position: absolute; + top: 5px; + background: #fff; +} + +thead .htCollapseButton:after { + height: 1px; + width: 700%; + right: 10px; + top: 4px; +} + +.handsontable tr th .htGroupStart:after { + background: transparent; + border-left: 1px solid #ccc; + border-top: 1px solid #ccc; + width: 5px; + position: relative; + top: 50%; +} + +.handsontable thead tr th .htGroupStart:after { + background: transparent; + border-left: 1px solid #ccc; + border-top: 1px solid #ccc; + height: 5px; + width: 50%; + position: relative; + top: 0px; + left: 50%; +} + +.handsontable .htGroupLevelTrigger { + -webkit-box-shadow: 1px 1px 3px rgba(0, 0, 0, 0.4); + box-shadow: 1px 1px 3px rgba(0, 0, 0, 0.4); + width: 15px; + height: 15px; + margin: 4px auto; + padding: 0px; + line-height: 15px; + cursor: pointer; +} + +.handsontable tr th .htExpandButton { + position: absolute; + width: 10px; + height: 10px; + line-height: 10px; + text-align: center; + border-radius: 5px; + border: 1px solid #f3f3f3; + -webkit-box-shadow: 1px 1px 3px rgba(0, 0, 0, 0.4); + box-shadow: 1px 1px 3px rgba(0, 0, 0, 0.4); + cursor: pointer; + top: 0px; + display: none; +} + +.handsontable thead tr th .htExpandButton { + /*left: 5px;*/ + top: 5px; +} + +.handsontable tr th .htExpandButton.clickable { + display: block; +} + +.handsontable col.hidden { + width: 0px !important; +} + +.handsontable tr.hidden, +.handsontable tr.hidden td, +.handsontable tr.hidden th { + display: none; +} + +/*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; + } +} + +/* + + Handsontable Mobile Text Editor stylesheet + + */ + +.handsontable.mobile { + -webkit-touch-callout:none; + -webkit-user-select:none; + -khtml-user-select:none; + -moz-user-select:none; + -ms-user-select:none; + user-select:none; + -webkit-tap-highlight-color:rgba(0,0,0,0); + -webkit-overflow-scrolling: touch; +} + +.htMobileEditorContainer { + display: none; + position: absolute; + top: 0; + width: 70%; + height: 54pt; + background: #f8f8f8; + border-radius: 20px; + border: 1px solid #ebebeb; + z-index: 999; + box-sizing: border-box; + -webkit-box-sizing: border-box; + -webkit-text-size-adjust: none; +} + +.topLeftSelectionHandle:not(.ht_master .topLeftSelectionHandle), +.topLeftSelectionHandle-HitArea:not(.ht_master .topLeftSelectionHandle-HitArea) { + z-index: 9999; +} + +/* Initial left/top coordinates - overwritten when actual position is set */ +.topLeftSelectionHandle, +.topLeftSelectionHandle-HitArea, +.bottomRightSelectionHandle, +.bottomRightSelectionHandle-HitArea { + left: -10000px; + top: -10000px; +} + +.htMobileEditorContainer.active { + display: block; +} + +.htMobileEditorContainer .inputs { + position: absolute; + right: 210pt; + bottom: 10pt; + top: 10pt; + left: 14px; + height: 34pt; +} + +.htMobileEditorContainer .inputs textarea { + font-size: 13pt; + border: 1px solid #a1a1a1; + -webkit-appearance: none; + -webkit-box-shadow: none; + -moz-box-shadow: none; + box-shadow: none; + position: absolute; + left: 14px; + right: 14px; + top: 0; + bottom: 0; + padding: 7pt; +} + +.htMobileEditorContainer .cellPointer { + position: absolute; + top: -13pt; + height: 0; + width: 0; + left: 30px; + + border-left: 13pt solid transparent; + border-right: 13pt solid transparent; + border-bottom: 13pt solid #ebebeb; +} + +.htMobileEditorContainer .cellPointer.hidden { + display: none; +} + +.htMobileEditorContainer .cellPointer:before { + content: ''; + display: block; + position: absolute; + top: 2px; + height: 0; + width: 0; + left: -13pt; + + border-left: 13pt solid transparent; + border-right: 13pt solid transparent; + border-bottom: 13pt solid #f8f8f8; +} + +.htMobileEditorContainer .moveHandle { + position: absolute; + top: 10pt; + left: 5px; + width: 30px; + bottom: 0px; + cursor: move; + z-index: 9999; +} + +.htMobileEditorContainer .moveHandle:after { + content: "..\a..\a..\a.."; + white-space: pre; + line-height: 10px; + font-size: 20pt; + display: inline-block; + margin-top: -8px; + color: #ebebeb; +} + +.htMobileEditorContainer .positionControls { + width: 205pt; + position: absolute; + right: 5pt; + top: 0; + bottom: 0; +} + +.htMobileEditorContainer .positionControls > div { + width: 50pt; + height: 100%; + float: left; +} + +.htMobileEditorContainer .positionControls > div:after { + content: " "; + display: block; + width: 15pt; + height: 15pt; + text-align: center; + line-height: 50pt; +} + +.htMobileEditorContainer .leftButton:after, +.htMobileEditorContainer .rightButton:after, +.htMobileEditorContainer .upButton:after, +.htMobileEditorContainer .downButton:after { + transform-origin: 5pt 5pt; + -webkit-transform-origin: 5pt 5pt; + margin: 21pt 0 0 21pt; +} + +.htMobileEditorContainer .leftButton:after { + border-top: 2px solid #288ffe; + border-left: 2px solid #288ffe; + -webkit-transform: rotate(-45deg); + /*margin-top: 17pt;*/ + /*margin-left: 20pt;*/ +} +.htMobileEditorContainer .leftButton:active:after { + border-color: #cfcfcf; +} + +.htMobileEditorContainer .rightButton:after { + border-top: 2px solid #288ffe; + border-left: 2px solid #288ffe; + -webkit-transform: rotate(135deg); + /*margin-top: 17pt;*/ + /*margin-left: 10pt;*/ +} +.htMobileEditorContainer .rightButton:active:after { + border-color: #cfcfcf; +} + +.htMobileEditorContainer .upButton:after { + /*border-top: 2px solid #cfcfcf;*/ + border-top: 2px solid #288ffe; + border-left: 2px solid #288ffe; + -webkit-transform: rotate(45deg); + /*margin-top: 22pt;*/ + /*margin-left: 15pt;*/ +} +.htMobileEditorContainer .upButton:active:after { + border-color: #cfcfcf; +} + +.htMobileEditorContainer .downButton:after { + border-top: 2px solid #288ffe; + border-left: 2px solid #288ffe; + -webkit-transform: rotate(225deg); + /*margin-top: 15pt;*/ + /*margin-left: 15pt;*/ +} +.htMobileEditorContainer .downButton:active:after { + border-color: #cfcfcf; +} + +.handsontable.hide-tween { + -webkit-animation: opacity-hide 0.3s; + animation: opacity-hide 0.3s; + animation-fill-mode: forwards; + -webkit-animation-fill-mode: forwards; +} + +.handsontable.show-tween { + -webkit-animation: opacity-show 0.3s; + animation: opacity-show 0.3s; + animation-fill-mode: forwards; + -webkit-animation-fill-mode: forwards; +} diff --git a/bower_components/handsontable/dist/handsontable.full.js b/bower_components/handsontable/dist/handsontable.full.js new file mode 100644 index 0000000..d5b496a --- /dev/null +++ b/bower_components/handsontable/dist/handsontable.full.js @@ -0,0 +1,21150 @@ +/*! + * Handsontable 0.12.4 + * Handsontable is a JavaScript library 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: Fri Jan 23 2015 10:07:24 GMT+0100 (CET) + */ +/*jslint white: true, browser: true, plusplus: true, indent: 4, maxerr: 50 */ + +//var Handsontable = { //class namespace +// plugins: {}, //plugin namespace +// helper: {} //helper namespace +//}; + +var Handsontable = function (rootElement, userSettings) { + userSettings = userSettings || {}; + var instance = new Handsontable.Core(rootElement, userSettings); + instance.init(); + return instance; +}; +Handsontable.plugins = {}; + +(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; + } + }; +} + +if (!Array.isArray) { + Array.isArray = function(obj) { + return toString.call(obj) == '[object Array]'; + }; +} + +// From https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/keys +// License CC-BY-SA v2.5 +if (!Object.keys) { + Object.keys = (function() { + 'use strict'; + var hasOwnProperty = Object.prototype.hasOwnProperty, + hasDontEnumBug = !({ toString: null }).propertyIsEnumerable('toString'), + dontEnums = [ + 'toString', + 'toLocaleString', + 'valueOf', + 'hasOwnProperty', + 'isPrototypeOf', + 'propertyIsEnumerable', + 'constructor' + ], + dontEnumsLength = dontEnums.length; + + return function(obj) { + if (typeof obj !== 'object' && (typeof obj !== 'function' || obj === null)) { + throw new TypeError('Object.keys called on non-object'); + } + + var result = [], prop, i; + + for (prop in obj) { + if (hasOwnProperty.call(obj, prop)) { + result.push(prop); + } + } + + if (hasDontEnumBug) { + for (i = 0; i < dontEnumsLength; i++) { + if (hasOwnProperty.call(obj, dontEnums[i])) { + result.push(dontEnums[i]); + } + } + } + return result; + }; + }()); +} + +/* + * 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 DOM 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 () {} + , eventManager = Handsontable.eventManager(instance); + + 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.insertBefore(this.container, rootElement.firstChild); + + this.guid = 'ht_' + Handsontable.helper.randomString(); //this is the namespace for global events + + if (!this.rootElement.id || this.rootElement.id.substring(0, 3) === "ht_") { + this.rootElement.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": + // //column order may have changes, so we need to translate the selection column index -> source array index + // index = instance.runHooksAndReturn('modifyCol', index); + delta = datamap.createCol(index, amount); + + if (delta) { + + if(Array.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": + //column order may have changes, so we need to translate the selection column index -> source array index + index = instance.runHooks('modifyCol', index); + + 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(Array.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; + + /* jshint ignore:start */ + default: + throw new Error('There is no such action "' + action + '"'); + break; + /* jshint ignore:end */ + } + + 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"] + * @param {String} direction (left|right|up|down) + * @param {Array} deltas array + * @return {Object|undefined} ending td in pasted area (only if any cell was changed) + */ + populateFromArray: function (start, input, end, source, method, direction, deltas) { + 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; + + /* jshint ignore:start */ + case 'overwrite': + default: + /* jshint ignore:end */ + // overwrite and other not specified options + current.row = start.row; + current.col = start.col; + + var iterators = {row: 0, col: 0}, // number of packages + selected = { // selected range + row: (end && start) ? (end.row - start.row + 1) : 1, + col: (end && start) ? (end.col - start.col + 1) : 1 + }; + + if (['up', 'left'].indexOf(direction) !== -1) { + iterators = { + row: Math.ceil(selected.row / rlen) || 1, + col: Math.ceil(selected.col / input[0].length) || 1 + }; + } else if (['down', 'right'].indexOf(direction) !== -1) { + iterators = { + row: 1, + col: 1 + }; + } + + + for (r = 0; r < rlen; r++) { + if ((end && current.row > end.row) || (!priv.settings.allowInsertRow && 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.allowInsertColumn && current.col > instance.countCols() - 1) || (current.col >= priv.settings.maxCols)) { + break; + } + + if (!instance.getCellMeta(current.row, current.col).readOnly) { + var result, + value = input[r][c], + index = { + row: r, + col: c + }; + + if (source === 'autofill') { + result = instance.runHooks('beforeAutofillInsidePopulate', index, direction, input, deltas, iterators, selected); + + if (result) { + iterators = typeof(result.iterators) !== 'undefined' ? result.iterators : iterators; + value = typeof(result.value) !== 'undefined' ? result.value : value; + } + } + + setData.push([current.row, current.col, value]); + } + + current.col++; + + if (end && c === clen - 1) { + c = -1; + + if (['down', 'right'].indexOf(direction) !== -1) { + iterators.col++; + } else if (['up', 'left'].indexOf(direction) !== -1) { + if (iterators.col > 1) { + iterators.col--; + } + } + + } + } + + current.row++; + iterators.col = 1; + + if (end && r === rlen - 1) { + r = -1; + + if (['down', 'right'].indexOf(direction) !== -1) { + iterators.row++; + } else if (['up', 'left'].indexOf(direction) !== -1) { + if (iterators.row > 1) { + iterators.row--; + } + } + + } + } + instance.setDataAtCell(setData, null, null, source || 'populateFromArray'); + break; + } + } + }; + + this.selection = selection = { //this public assignment is only temporary + inProgress: false, + + selectedHeader: { + cols: false, + rows: false + }, + + setSelectedHeaders: function (rows, cols) { + instance.selection.selectedHeader.rows = rows; + instance.selection.selectedHeader.cols = cols; + }, + + /** + * 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, keepEditorOpened) { + Handsontable.hooks.run(instance, "beforeSetRangeStart", coords); + priv.selRange = new WalkontableCellRange(coords, coords, coords); + selection.setRangeEnd(coords, null, keepEditorOpened); + }, + + /** + * 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, keepEditorOpened) { + //trigger handlers + Handsontable.hooks.run(instance, "beforeSetRangeEnd", coords); + + instance.selection.begin(); + + priv.selRange.to = new WalkontableCellCoords(coords.row, coords.col); + 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()) { + if(priv.selRange.from) { + instance.view.scrollViewport(priv.selRange.from); + } else { + instance.view.scrollViewport(coords); + } + + } + selection.refreshBorders(null, keepEditorOpened); + }, + + /** + * 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 () { + var isMultiple = !(priv.selRange.to.col === priv.selRange.from.col && priv.selRange.to.row === priv.selRange.from.row) + , modifier = Handsontable.hooks.run(instance, 'afterIsMultipleSelection', isMultiple); + + if(isMultiple) { + return modifier; + } + }, + + /** + * Selects cell relative to current cell (if possible) + */ + transformStart: function (rowDelta, colDelta, force, keepEditorOpened) { + var delta = new WalkontableCellCoords(rowDelta, colDelta); + instance.runHooks('modifyTransformStart', delta); + + /* jshint ignore:start */ + 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; + } + /* jshint ignore:end */ + + 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, keepEditorOpened); + }, + + /** + * 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'); + + if(Handsontable.mobileBrowser) { + Handsontable.Dom.addClass(instance.rootElement, 'mobile'); + } + + 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 () { + /* jshint ignore:start */ + if (this.validatorsInQueue == 0 && resolved == false) { + resolved = true; + this.onQueueEmpty(); + } + /* jshint ignore:end */ + } + }; + } + + 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]); + // column order may have changes, so we need to translate physical col index (stored in datasource) to logical (displayed to user) + var logicalCol = instance.runHooks('modifyCol', col); + 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]) || cellProperties.format )) { + var len = changes[i][3].length; + if (typeof cellProperties.language == 'undefined') { + numeral.language('en'); + } + // this input in format XXXX.XX is likely to come from paste. Let's parse it using international rules + else if (changes[i][3].indexOf(".") === len - 3 && changes[i][3].indexOf(",") === -1) { + numeral.language('en'); + } + else { + numeral.language(cellProperties.language); + } + if (numeral.validate(changes[i][3])) { + changes[i][3] = numeral().unformat(changes[i][3]); + } + } + } + + /* jshint ignore:start */ + 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); + } + /* jshint ignore:end */ + } + } + waitingForValidator.checkIfQueueIsEmpty(); + + function resolve() { + var beforeChangeResult; + + if (changes.length) { + beforeChangeResult = Handsontable.hooks.run(instance, "beforeChange", changes, source); + if (typeof beforeChangeResult === 'function') { + console.warn("Your beforeChange callback returns a function. It's not supported since Handsontable 0.12.1 (and the returned function will not be executed)."); + } else if (beforeChangeResult === false) { + changes.splice(0, changes.length); //invalidate all changes (remove everything from array) + } + } + 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.allowInsertRow) { + while (changes[i][0] > instance.countRows() - 1) { + datamap.createRow(); + } + } + + if (instance.dataType === 'array' && priv.settings.allowInsertColumn) { + 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.run(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) { + valid = Handsontable.hooks.run(instance, "afterValidate", valid, value, cellProperties.row, cellProperties.prop, source); + cellProperties.valid = valid; + + callback(valid); + Handsontable.hooks.run(instance, "postAfterValidate", valid, value, cellProperties.row, cellProperties.prop, source); + }); + }, 0)); + + } else { + //resolve callback even if validator function was not found + cellProperties.valid = true; + callback(true); + } + }; + + function setDataInputToArray(row, propOrCol, value) { + if (typeof row === "object") { //is it an array of changes + return row; + } + else { + return [ + [row, propOrCol, 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"] + * @param {String} direction edit (left|right|up|down) + * @param {Array} deltas array + * @return {Object|undefined} ending td in pasted area (only if any cell was changed) + */ + this.populateFromArray = function (row, col, input, endRow, endCol, source, method, direction, deltas) { + var c; + + 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 + } + c = typeof endRow === 'number' ? new WalkontableCellCoords(endRow, endCol) : null; + + return grid.populateFromArray(new WalkontableCellCoords(row, col), input, c, source, method, direction, deltas); + }; + + /** + * 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/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/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 (Array.isArray(priv.settings.dataSchema) || Array.isArray(data[0])) { + 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' || Array.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') { + /* jshint -W089 */ + 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) { + Handsontable.Dom.removeClass(instance.rootElement,GridSettings.prototype.className); +// instance.rootElement.removeClass(GridSettings.prototype.className); + } + if (settings.className) { + Handsontable.Dom.addClass(instance.rootElement,settings.className); +// instance.rootElement.addClass(settings.className); + } + } + + if (typeof settings.height != 'undefined'){ + var height = settings.height; + + if (typeof height == 'function'){ + height = height(); + } + + instance.rootElement.style.height = height + 'px'; + } + + if (typeof settings.width != 'undefined'){ + var width = settings.width; + + if (typeof width == 'function'){ + width = width(); + } + + instance.rootElement.style.width = width + 'px'; + } + + /* jshint ignore:start */ + if (height){ + instance.rootElement.style.overflow = 'auto'; + } + /* jshint ignore:end */ + + 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')) { + // ignore obj.prototype.type + return; + } + + 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 element corresponding to params row, col + * @param {Number} row + * @param {Number} col + * @param {Boolean} topmost + * @public + * @return {Element} + */ + this.getCell = function (row, col, topmost) { + return instance.view.getCellAtCoords(new WalkontableCellCoords(row, col), topmost); + }; + + /** + * Returns coordinates for the provided element + * @param elem + * @returns {WalkontableCellCoords|*} + */ + this.getCoords = function(elem) { + return this.view.wt.wtTable.getCoords.call(this.view.wt.wtTable, elem); + }; + + /** + * 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 = [], + range; + + range = datamap.getRange( + new WalkontableCellCoords(0, datamap.propToCol(prop)), + new WalkontableCellCoords(priv.settings.data.length - 1, datamap.propToCol(prop)), + datamap.DESTINATION_RENDERER); + + return out.concat.apply(out, range); + }; + + /** + * 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); + /* jshint ignore:start */ + if(cellMeta[key] != undefined){ + delete priv.cellSettings[row][col][key]; + } + /* jshint ignore:end */ + }; + + /** + * 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') { + /* jshint -W089 */ + for (var key in prop) { + var value = prop[key]; + 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.run(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){ + // warning: this must be done after datamap.colToProp + return Handsontable.hooks.run(instance, 'modifyCol', col); + } + + 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; + + /* jshint ignore:start */ + 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--; + } + /* jshint ignore:end */ + 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 (Array.isArray(priv.settings.rowHeaders) && 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.run(instance, 'modifyCol', col); + + if (priv.settings.columns && priv.settings.columns[col] && priv.settings.columns[col].title) { + return priv.settings.columns[col].title; + } + else if (Array.isArray(priv.settings.colHeaders) && 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.run(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); + + height = Handsontable.hooks.run(instance, 'modifyRowHeight', height, row); + + return height; + }; + + /** + * 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 rendered row + * @return {Number} + */ + this.rowOffset = function () { + return instance.view.wt.wtTable.getFirstRenderedRow(); + }; + + /** + * Return index of first visible column + * @return {Number} + */ + this.colOffset = function () { + return instance.view.wt.wtTable.getFirstRenderedColumn(); + }; + + /** + * Return number of rendered rows (including rows partially or fully rendered outside viewport). Returns -1 if table is not visible + * @return {Number} + */ + this.countRenderedRows = function () { + return instance.view.wt.drawn ? instance.view.wt.wtTable.getRenderedRowsCount() : -1; + }; + + /** + * Return number of visible rows (rendered rows that fully fit inside viewport)). Returns -1 if table is not visible + * @return {Number} + */ + this.countVisibleRows = function () { + return instance.view.wt.drawn ? instance.view.wt.wtTable.getVisibleRowsCount() : -1; + }; + + /** + * Return number of rendered columns (including columns partially or fully rendered outside viewport). Returns -1 if table is not visible + * @return {Number} + */ + this.countRenderedCols = function () { + return instance.view.wt.drawn ? instance.view.wt.wtTable.getRenderedColumnsCount() : -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.getVisibleColumnsCount() : - 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.run(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) { + /* jshint ignore:start */ + arguments[1] = datamap.propToCol(arguments[1]); + if (typeof arguments[3] !== "undefined") { + arguments[3] = datamap.propToCol(arguments[3]); + } + return instance.selectCell.apply(instance, arguments); + /* jshint ignore:end */ + }; + + /** + * 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.destroy(); + } + + + Handsontable.Dom.empty(instance.rootElement); + eventManager.clear(); + + 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") { + 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; + }; + + /** + * 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) { + return Handsontable.hooks.run(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) { + return elem; + } + elem = elem.parentNode; + } + return null; +}; + +/** + * Goes up the DOM tree and checks if element is child of another element + * @param child Child element + * @param {Object|string} parent Parent element OR selector of the parent element. If classname provided, function returns true for the first occurance of element with that class. + * @returns {boolean} + */ +Handsontable.Dom.isChildOf = function (child, parent) { + var node = child.parentNode; + var queriedParents = []; + if(typeof parent === "string") { + queriedParents = Array.prototype.slice.call(document.querySelectorAll(parent), 0); + } else { + queriedParents.push(parent); + } + + while (node != null) { + if (queriedParents.indexOf(node) > - 1) { + 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; + if (elem.previousSibling) { + /* jshint ignore:start */ + while (elem = elem.previousSibling) { + ++i; + } + /* jshint ignore:end */ + } + 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) { + if (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 + } + }; +} + +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; + /* jshint ignore:start */ + while (child = element.lastChild) { + element.removeChild(child); + } + /* jshint ignore:end */ +}; + +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 if element is attached to the DOM and visible, false otherwise + * @param elem + * @returns {boolean} + */ +Handsontable.Dom.isVisible = function (elem) { + 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. Function is not compatible with jQuery offset. + * + * @param {HTMLElement} elem + * @return {Object} Returns object with `top` and `left` props + */ +Handsontable.Dom.offset = function (elem) { + var offsetLeft, + offsetTop, + lastElem, + docElem, + box; + + docElem = document.documentElement; + + 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 + box = elem.getBoundingClientRect(); + + return { + top: box.top + (window.pageYOffset || docElem.scrollTop) - (docElem.clientTop || 0), + left: box.left + (window.pageXOffset || docElem.scrollLeft) - (docElem.clientLeft || 0) + }; + } + offsetLeft = elem.offsetLeft; + offsetTop = elem.offsetTop; + lastElem = elem; + + /* jshint ignore:start */ + while (elem = elem.offsetParent) { + // from my observation, document.body always has scrollLeft/scrollTop == 0 + if (elem === document.body) { + break; + } + offsetLeft += elem.offsetLeft; + offsetTop += elem.offsetTop; + lastElem = elem; + } + /* jshint ignore:end */ + + //slow - http://jsperf.com/offset-vs-getboundingclientrect/6 + if (lastElem && lastElem.style.position === 'fixed') { + //if(lastElem !== document.body) { //faster but does gives false positive in Firefox + offsetLeft += window.pageXOffset || docElem.scrollLeft; + offsetTop += window.pageYOffset || docElem.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.getScrollableElement = function (element) { + var el = element.parentNode, + props = ['auto', 'scroll'], + overflow, overflowX, overflowY; + + while (el && el.style) { + overflow = el.style.overflow; + overflowX = el.style.overflowX; + overflowY = el.style.overflowY; + + if (overflow == 'scroll' || overflowX == 'scroll' || overflowY == 'scroll') { + return el; + } + if (el.clientHeight < el.scrollHeight && (props.indexOf(overflowY) !== -1 || props.indexOf(overflow) !== -1)) { + return el; + } + if (el.clientWidth < el.scrollWidth && (props.indexOf(overflowX) !== -1 || props.indexOf(overflow) !== -1)) { + return el; + } + el = el.parentNode; + } + + return window; +}; + +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 || elem.innerHeight; +}; + +Handsontable.Dom.innerWidth = function (elem) { + return elem.clientWidth || elem.innerWidth; +}; + +Handsontable.Dom.addEvent = function(element, event, callback) { + if (window.addEventListener) { + element.addEventListener(event, callback, false); + } else { + element.attachEvent('on' + event, callback); + } +}; + +Handsontable.Dom.removeEvent = function(element, event, callback) { + if (window.removeEventListener) { + element.removeEventListener(event, callback, false); + } else { + element.detachEvent('on' + event, callback); + } +}; + + +(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'; + + 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; + }; + + /** + * Returns end of the selection in text input + * @return {Number} + */ + Handsontable.Dom.getSelectionEndPosition = function (el) { + if(el.selectionEnd) { + return el.selectionEnd; + } else if(document.selection) { //IE8 + var r = document.selection.createRange(); + if(r == null) { + return 0; + } + var re = el.createTextRange(); + + return re.text.indexOf(r.text) + r.text.length; + } + }; + + /** + * 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; + }; + + var isIE8 = !(document.createTextNode('test').textContent); + Handsontable.Dom.isIE8 = function () { + return isIE8; + }; + + var isIE9 = !!(document.documentMode); + Handsontable.Dom.isIE9 = function () { + return isIE9; + }; + + var isSafari = (/Safari/.test(navigator.userAgent) && /Apple Computer/.test(navigator.vendor)); + Handsontable.Dom.isSafari = function () { + return isSafari; + }; + + /** + * Sets overlay position depending on it's type and used browser + */ + Handsontable.Dom.setOverlayPosition = function (overlayElem, left, top) { + if (isIE8 || isIE9) { + overlayElem.style.top = top; + overlayElem.style.left = left; + } else if (isSafari) { + overlayElem.style['-webkit-transform'] = 'translate3d(' + left + ',' + top + ',0)'; + } else { + overlayElem.style['transform'] = 'translate3d(' + left + ',' + top + ',0)'; + } + }; + + Handsontable.Dom.getCssTransform = function (elem) { + var transform; + + /* jshint ignore:start */ + if(elem.style['transform'] && (transform = elem.style['transform']) != "") { + return ['transform', transform]; + } else if (elem.style['-webkit-transform'] && (transform = elem.style['-webkit-transform']) != "") { + return ['-webkit-transform', transform]; + } else { + return -1; + } + /* jshint ignore:end */ + }; + + Handsontable.Dom.resetCssTransform = function (elem) { + /* jshint ignore:start */ + if(elem['transform'] && elem['transform'] != "") { + elem['transform'] = ""; + } else if(elem['-webkit-transform'] && elem['-webkit-transform'] != "") { + elem['-webkit-transform'] = ""; + } + /* jshint ignore:end */ + }; + +})(); + + +if(!window.Handsontable){ + var Handsontable = {}; +} + +Handsontable.countEventManagerListeners = 0; //used to debug memory leaks + +Handsontable.eventManager = function (instance) { + var + addEvent, + removeEvent, + clearEvents, + fireEvent; + + if (!instance) { + throw new Error ('instance not defined'); + } + if (!instance.eventListeners) { + instance.eventListeners = []; + } + + /** + * Add Event + * + * @param {Element} element + * @param {String} event + * @param {Function} callback + * @returns {Function} Returns function which you can easily call to remove that event + */ + addEvent = function (element, event, callback) { + var callbackProxy; + + callbackProxy = function (event) { + if (event.target == void 0 && event.srcElement != void 0) { + if (event.definePoperty) { + event.definePoperty('target', { + value: event.srcElement + }); + } else { + event.target = event.srcElement; + } + } + + if (event.preventDefault == void 0) { + if (event.definePoperty) { + event.definePoperty('preventDefault', { + value: function() { + this.returnValue = false; + } + }); + } else { + event.preventDefault = function () { + this.returnValue = false; + }; + } + } + callback.call(this, event); + }; + + instance.eventListeners.push({ + element: element, + event: event, + callback: callback, + callbackProxy: callbackProxy + }); + + if (window.addEventListener) { + element.addEventListener(event, callbackProxy, false); + } else { + element.attachEvent('on' + event, callbackProxy); + } + Handsontable.countEventManagerListeners ++; + + return function _removeEvent() { + removeEvent(element, event, callback); + }; + }; + + /** + * Remove event + * + * @param {Element} element + * @param {String} event + * @param {Function} callback + */ + removeEvent = function (element, event, callback) { + var len = instance.eventListeners.length, + tmpEvent; + + while (len--) { + tmpEvent = instance.eventListeners[len]; + + if (tmpEvent.event == event && tmpEvent.element == element) { + if (callback && callback != tmpEvent.callback) { + continue; + } + instance.eventListeners.splice(len, 1); + + if (tmpEvent.element.removeEventListener) { + tmpEvent.element.removeEventListener(tmpEvent.event, tmpEvent.callbackProxy, false); + } else { + tmpEvent.element.detachEvent('on' + tmpEvent.event, tmpEvent.callbackProxy); + } + Handsontable.countEventManagerListeners --; + } + } + }; + + /** + * Clear all events + */ + clearEvents = function () { + var len = instance.eventListeners.length, + event; + + while (len--) { + event = instance.eventListeners[len]; + + if (event) { + removeEvent(event.element, event.event, event.callback); + } + } + }; + + /** + * Trigger event + * + * @param {Element} element + * @param {String} type + */ + fireEvent = function (element, type) { + var options, event; + + options = { + bubbles: true, + cancelable: (type !== "mousemove"), + view: window, + detail: 0, + screenX: 0, + screenY: 0, + clientX: 1, + clientY: 1, + ctrlKey: false, + altKey: false, + shiftKey: false, + metaKey: false, + button: 0, + relatedTarget: undefined + }; + + if (document.createEvent) { + event = document.createEvent("MouseEvents"); + event.initMouseEvent(type, options.bubbles, options.cancelable, + options.view, options.detail, + options.screenX, options.screenY, options.clientX, options.clientY, + options.ctrlKey, options.altKey, options.shiftKey, options.metaKey, + options.button, options.relatedTarget || document.body.parentNode); + + } else { + event = document.createEventObject(); + } + + if (element.dispatchEvent) { + element.dispatchEvent(event); + } else { + element.fireEvent('on' + type, event); + } + }; + + return { + addEventListener: addEvent, + removeEventListener: removeEvent, + clear: clearEvents, + fireEvent: fireEvent + }; +}; + +/** + * Handsontable TableView constructor + * @param {Object} instance + */ +Handsontable.TableView = function (instance) { + var that = this; + + this.eventManager = Handsontable.eventManager(instance); + this.instance = instance; + this.settings = instance.getSettings(); + + + var originalStyle = instance.rootElement.getAttribute('style'); + if(originalStyle) { + instance.rootElement.setAttribute('data-originalstyle', originalStyle); //needed to retrieve original style in jsFiddle link generator in HT examples. may be removed in future versions + } + + Handsontable.Dom.addClass(instance.rootElement,'handsontable'); +// 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.insertBefore(table, instance.container.firstChild); + + this.eventManager.addEventListener(instance.rootElement,'mousedown', 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. + } + }); + + this.eventManager.addEventListener(document.documentElement, 'keyup',function (event) { + if (instance.selection.isInProgress() && !event.shiftKey) { + instance.selection.finish(); + } + }); + + var isMouseDown; + this.isMouseDown = function () { + return isMouseDown; + }; + + this.eventManager.addEventListener(document.documentElement, 'mouseup', 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(); + } + }); + + this.eventManager.addEventListener(document.documentElement, 'mousedown',function (event) { + var next = event.target; + + if (isMouseDown) { + return; //it must have been started in a cell + } + + 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) { + 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(); + } + }); + + + + this.eventManager.addEventListener(table, 'selectstart', function (event) { + if (that.settings.fragmentSelection) { + return; + } + + //https://github.com/handsontable/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 selections = [ + new WalkontableSelection({ + className: 'current', + border: { + width: 2, + color: '#5292F7', + //style: 'solid', //not used + cornerVisible: function () { + return that.settings.fillHandle && !that.isCellEdited() && !instance.selection.isMultiple(); + }, + multipleSelectionHandlesVisible: function () { + return !that.isCellEdited() && !instance.selection.isMultiple(); + } + } + }), + new WalkontableSelection({ + className: 'area', + border: { + width: 1, + color: '#89AFF9', + //style: 'solid', // not used + cornerVisible: function () { + return that.settings.fillHandle && !that.isCellEdited() && instance.selection.isMultiple(); + }, + multipleSelectionHandlesVisible: function () { + return !that.isCellEdited() && instance.selection.isMultiple(); + } + } + }), + new WalkontableSelection({ + className: 'highlight', + highlightRowClassName: that.settings.currentRowClassName, + highlightColumnClassName: that.settings.currentColClassName + }), + new WalkontableSelection({ + className: 'fill', + border: { + width: 1, + color: 'red' + //style: 'solid' // not used + } + }) + ]; + selections.current = selections[0]; + selections.area = selections[1]; + selections.highlight = selections[2]; + selections.fill = selections[3]; + + var walkontableConfig = { + debug: function () { + return that.settings.debug; + }, + table: table, + stretchH: this.settings.stretchH, + data: instance.getDataAtCell, + totalRows: instance.countRows, + totalColumns: instance.countCols, + fixedColumnsLeft: function () { + return that.settings.fixedColumnsLeft; + }, + fixedRowsTop: function () { + return that.settings.fixedRowsTop; + }, + renderAllRows: that.settings.renderAllRows, + rowHeaders: function () { + var arr = []; + if(instance.hasRowHeaders()) { + arr.push(function (index, TH) { + that.appendRowHeader(index, TH); + }); + } + Handsontable.hooks.run(instance, 'afterGetRowHeaderRenderers', arr); + return arr; + }, + columnHeaders: function () { + + var arr = []; + if(instance.hasColHeaders()) { + arr.push(function (index, TH) { + that.appendColHeader(index, TH); + }); + } + Handsontable.hooks.run(instance, 'afterGetColumnHeaderRenderers', arr); + return arr; + }, + 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: selections, + hideBorderOnMouseDownOver: function () { + return that.settings.fragmentSelection; + }, + onCellMouseDown: function (event, coords, TD, wt) { + instance.listen(); + that.activeWt = wt; + + isMouseDown = true; + + Handsontable.hooks.run(instance, 'beforeOnCellMouseDown', event, coords, TD); + + Handsontable.Dom.enableImmediatePropagation(event); + + if (!event.isImmediatePropagationStopped()) { + + 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); + instance.selection.setSelectedHeaders(false, true); + } + if (coords.col < 0) { + instance.selectCell(coords.row, 0, coords.row, instance.countCols() - 1); + instance.selection.setSelectedHeaders(true, false); + } + } + 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); + } + } else { + if (isMouseDown) { + // multi select columns + if (coords.row < 0) { + instance.selection.setRangeEnd(new WalkontableCellCoords(instance.countRows() - 1, coords.col)); + instance.selection.setSelectedHeaders(false, true); + } + + // multi select rows + if (coords.col < 0) { + instance.selection.setRangeEnd(new WalkontableCellCoords(coords.row, instance.countCols() - 1)); + instance.selection.setSelectedHeaders(true, false); + } + } + } + + 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'); + }, + onBeforeDrawBorders: function (corners, borderClassName) { + instance.runHooks('beforeDrawBorders', corners, borderClassName); + }, + onBeforeTouchScroll: function () { + instance.runHooks('beforeTouchScroll'); + }, + onAfterMomentumScroll: function () { + instance.runHooks('afterMomentumScroll'); + }, + viewportRowCalculatorOverride: function (calc) { + if (that.settings.viewportRowRenderingOffset) { + calc.startRow = Math.max(calc.startRow - that.settings.viewportRowRenderingOffset, 0); + calc.endRow = Math.min(calc.endRow + that.settings.viewportRowRenderingOffset, instance.countRows() - 1); + } + instance.runHooks('afterViewportRowCalculatorOverride', calc); + }, + viewportColumnCalculatorOverride: function (calc) { + if (that.settings.viewportColumnRenderingOffset) { + calc.startColumn = Math.max(calc.startColumn - that.settings.viewportColumnRenderingOffset, 0); + calc.endColumn = Math.min(calc.endColumn + that.settings.viewportColumnRenderingOffset, instance.countCols() - 1); + } + instance.runHooks('afterViewportColumnCalculatorOverride', calc); + } + }; + + Handsontable.hooks.run(instance, 'beforeInitWalkontable', walkontableConfig); + + this.wt = new Walkontable(walkontableConfig); + this.activeWt = this.wt; + + this.eventManager.addEventListener(that.wt.wtTable.spreader, 'mousedown', 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 + Handsontable.helper.stopPropagation(event); + //event.stopPropagation(); + } + }); + + this.eventManager.addEventListener(that.wt.wtTable.spreader, 'contextmenu', 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 + Handsontable.helper.stopPropagation(event); + //event.stopPropagation(); + } + }); + + + this.eventManager.addEventListener(document.documentElement, 'click', 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.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 + * @param {Boolean} topmost + */ +Handsontable.TableView.prototype.getCellAtCoords = function (coords, topmost) { + var td = this.wt.getCell(coords, topmost); + //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, String.fromCharCode(160)); // workaround for https://github.com/handsontable/handsontable/issues/1946 + } + + 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'; + + if (col > -1) { + Handsontable.Dom.fastInnerHTML(SPAN, this.instance.getColHeader(col)); + } else { + Handsontable.Dom.fastInnerText(SPAN, String.fromCharCode(160)); // workaround for https://github.com/handsontable/handsontable/issues/1946 + } + 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} leftOffset + * @return {Number} + */ +Handsontable.TableView.prototype.maximumVisibleElementWidth = function (leftOffset) { + 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) { + 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; +}; + +Handsontable.TableView.prototype.destroy = function () { + this.wt.destroy(); + this.eventManager.clear(); +}; + +/** + * 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 keyCodes = Handsontable.helper.keyCode; + var destroyed = false; + + var eventManager = Handsontable.eventManager(instance); + + var activeEditor; + + var init = function () { + + function onKeyDown(event) { + + if (!instance.isListening()) { + return; + } + + Handsontable.hooks.run(instance, 'beforeKeyDown', event); + + if(destroyed) { + return; + } + + Handsontable.Dom.enableImmediatePropagation(event); + + 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(""); + return; + } + } + + var rangeModifier = event.shiftKey ? selection.setRangeEnd : selection.setRangeStart; + + switch (event.keyCode) { + + case keyCodes.A: + if (ctrlDown) { + selection.selectAll(); //select all cells + + event.preventDefault(); + Handsontable.helper.stopPropagation(event); + //event.stopPropagation(); + } + break; + + case keyCodes.ARROW_UP: + + if (that.isEditorOpened() && !activeEditor.isWaiting()){ + that.closeEditorAndSaveChanges(ctrlDown); + } + + moveSelectionUp(event.shiftKey); + + event.preventDefault(); + Handsontable.helper.stopPropagation(event); + //event.stopPropagation(); //required by HandsontableEditor + break; + + case keyCodes.ARROW_DOWN: + if (that.isEditorOpened() && !activeEditor.isWaiting()){ + that.closeEditorAndSaveChanges(ctrlDown); + } + + moveSelectionDown(event.shiftKey); + + event.preventDefault(); + Handsontable.helper.stopPropagation(event); + //event.stopPropagation(); //required by HandsontableEditor + break; + + case keyCodes.ARROW_RIGHT: + if(that.isEditorOpened() && !activeEditor.isWaiting()){ + that.closeEditorAndSaveChanges(ctrlDown); + } + + moveSelectionRight(event.shiftKey); + + event.preventDefault(); + Handsontable.helper.stopPropagation(event); + //event.stopPropagation(); //required by HandsontableEditor + break; + + case keyCodes.ARROW_LEFT: + if(that.isEditorOpened() && !activeEditor.isWaiting()){ + that.closeEditorAndSaveChanges(ctrlDown); + } + + moveSelectionLeft(event.shiftKey); + + event.preventDefault(); + Handsontable.helper.stopPropagation(event); + //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(); + Handsontable.helper.stopPropagation(event); + //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 + Handsontable.helper.stopPropagation(event); + //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 + Handsontable.helper.stopPropagation(event); + //event.stopPropagation(); //required by HandsontableEditor + break; + + case keyCodes.PAGE_UP: + selection.transformStart(-instance.countVisibleRows(), 0); + event.preventDefault(); //don't page up the window + Handsontable.helper.stopPropagation(event); + //event.stopPropagation(); //required by HandsontableEditor + break; + + case keyCodes.PAGE_DOWN: + selection.transformStart(instance.countVisibleRows(), 0); + event.preventDefault(); //don't page down the window + Handsontable.helper.stopPropagation(event); + //event.stopPropagation(); //required by HandsontableEditor + break; + } + + } + } + } + + instance.addHook('afterDocumentKeyDown', function(originalEvent){ + onKeyDown(originalEvent); + }); + + eventManager.addEventListener(document, 'keydown', function (ev){ + instance.runHooks('afterDocumentKeyDown', ev); + }); + + function onDblClick(event, coords, elem) { + if(elem.nodeName == "TD") { //may be TD or TH + 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); + +Handsontable.helper = {}; + +/** + * 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 + ''; + + case 'object': + if (value === null) { + return ''; + } + else { + return value.toString(); + } + break; + case 'undefined': + return ''; + + 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; +}; + +/** + * Creates 2D array of Excel-like values "A1", "A2", ... + * @param rowCount + * @param colCount + * @returns {Array} + */ +Handsontable.helper.createSpreadsheetData = function(rowCount, colCount) { + rowCount = typeof rowCount === 'number' ? rowCount : 100; + colCount = typeof colCount === 'number' ? colCount : 4; + + var rows = [] + , i + , j; + + for (i = 0; i < rowCount; i++) { + var row = []; + for (j = 0; j < colCount; j++) { + row.push(Handsontable.helper.spreadsheetColumnLabel(j) + (i + 1)); + } + rows.push(row); + } + return rows; +}; + +Handsontable.helper.createSpreadsheetObjectData = function(rowCount, colCount) { + rowCount = typeof rowCount === 'number' ? rowCount : 100; + colCount = typeof colCount === 'number' ? colCount : 4; + + var rows = [] + , i + , j; + + for (i = 0; i < rowCount; i++) { + var row = {}; + for (j = 0; j < colCount; j++) { + row['prop' + j] = Handsontable.helper.spreadsheetColumnLabel(j) + (i + 1); + } + rows.push(row); + } + return rows; +}; + +/** + * 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; +}; + +/** + * 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/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]; + } + } +}; + +/** + * Perform deep 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.deepExtend = function (target, extension) { + for (var key in extension) { + if (extension.hasOwnProperty(key)) { + if (extension[key] && typeof extension[key] === 'object') { + if (!target[key]) { + if (Array.isArray(extension[key])) { + target[key] = []; + } + else { + target[key] = {}; + } + } + Handsontable.helper.deepExtend(target[key], extension[key]); + } + else { + target[key] = extension[key]; + } + } + } +}; + +/** + * Perform deep clone of an object + * WARNING! Only clones JSON properties. Will cause error when `obj` contains a function, Date, etc + * @param {Object} obj An object that will be cloned + * @return {Object} + */ +Handsontable.helper.deepClone = function (obj) { + if (typeof obj === "object") { + return JSON.parse(JSON.stringify(obj)); + } + else { + return obj; + } +}; + +Handsontable.helper.getPrototypeOf = function (obj) { + var prototype; + + /* jshint ignore:start */ + 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 + + } + /* jshint ignore:end */ + + 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, + INSERT: 45, + 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]'; +}; + +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.isMobileBrowser = function (userAgent) { + if(!userAgent) { + userAgent = navigator.userAgent; + } + return (/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(userAgent)); + + // Logic for checking the specific mobile browser + // + /* var type = type != void 0 ? type.toLowerCase() : '' + , result; + switch(type) { + case '': + result = (/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)); + return result; + break; + case 'ipad': + return navigator.userAgent.indexOf('iPad') > -1; + break; + case 'android': + return navigator.userAgent.indexOf('Android') > -1; + break; + case 'windows': + return navigator.userAgent.indexOf('IEMobile') > -1; + break; + default: + throw new Error('Invalid isMobileBrowser argument'); + break; + } */ +}; + +Handsontable.helper.isTouchSupported = function () { + return ('ontouchstart' in window); +}; + +Handsontable.helper.stopPropagation = function (event) { + // ie8 + //http://msdn.microsoft.com/en-us/library/ie/ff975462(v=vs.85).aspx + if (typeof (event.stopPropagation) === 'function') { + event.stopPropagation(); + } + else { + event.cancelBubble = true; + } +}; + +Handsontable.helper.pageX = function (event) { + if (event.pageX) { + return event.pageX; + } + + var scrollLeft = Handsontable.Dom.getWindowScrollLeft(); + var cursorX = event.clientX + scrollLeft; + + return cursorX; +}; + +Handsontable.helper.pageY = function (event) { + if (event.pageY) { + return event.pageY; + } + + var scrollTop = Handsontable.Dom.getWindowScrollTop(); + var cursorY = event.clientY + scrollTop; + + return cursorY; +}; + +(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 (!Array.isArray(obj)){ + schema = {}; + for (var i in obj) { + if (obj.hasOwnProperty(i)) { + if (typeof obj[i] === "object" && !Array.isArray(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 (typeof schema === "object" && !Array.isArray(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 () { + var i, ilen, schema = this.getSchema(); + if (typeof schema === "undefined") { + throw new Error("trying to create `columns` definition but you didnt' provide `schema` nor `data`"); + } + 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.run(this.instance, 'modifyCol', col); + + if (this.colToPropCache && typeof this.colToPropCache[col] !== 'undefined') { + return this.colToPropCache[col]; + } + + 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.run(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 = {}; + Handsontable.helper.deepExtend(row, 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.run(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.run(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.run(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.run(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++) { + + if (typeof out[sliced[i]] === 'undefined'){ + out[sliced[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.run(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 properties (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/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(String.fromCharCode(9660))); // workaround for https://github.com/handsontable/handsontable/issues/1946 +//this is faster than innerHTML. See: https://github.com/handsontable/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(String.fromCharCode(160))); // workaround for https://github.com/handsontable/handsontable/issues/1946 + //this is faster than innerHTML. See: https://github.com/handsontable/handsontable/wiki/JavaScript-&-DOM-performance-tips + } + + + + if (!instance.acArrowListener) { + var eventManager = Handsontable.eventManager(instance); + + //not very elegant but easy and fast + instance.acArrowListener = function (event) { + if (Handsontable.Dom.hasClass(event.target,'htAutocompleteArrow')) { + instance.view.wt.getSetting('onCellDblClick', null, new WalkontableCellCoords(row, col), TD); + } + }; + + eventManager.addEventListener(instance.rootElement,'mousedown',instance.acArrowListener); + + //We need to unbind the listener after the table has been destroyed + instance.addHookOnce('afterDestroy', function () { + eventManager.clear(); + }); + + } + }; + + 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) { + + var eventManager = Handsontable.eventManager(instance); + + 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/handsontable/wiki/JavaScript-&-DOM-performance-tips + } + + if (cellProperties.readOnly) { + eventManager.addEventListener(INPUT,'click',function (event) { + event.preventDefault(); + }); + } + else { + eventManager.addEventListener(INPUT,'mousedown',function (event) { + Handsontable.helper.stopPropagation(event); + //event.stopPropagation(); //otherwise can confuse cell mousedown handler + }); + + eventManager.addEventListener(INPUT,'mouseup',function (event) { + Handsontable.helper.stopPropagation(event); + //event.stopPropagation(); //otherwise can confuse cell dblclick handler + }); + + eventManager.addEventListener(INPUT,'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){ + + Handsontable.Dom.enableImmediatePropagation(event); + + if(event.keyCode == Handsontable.helper.keyCode.SPACE || event.keyCode == Handsontable.helper.keyCode.ENTER){ + + 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; + eventManager.fireEvent(checkbox[i], '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 properties (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(Handsontable){ + + '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); + + }; + + Handsontable.PasswordRenderer = PasswordRenderer; + Handsontable.renderers.PasswordRenderer = PasswordRenderer; + Handsontable.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() + , tmp; + + if(sel[0] > sel[2]) { + tmp = sel[0]; + sel[0] = sel[2]; + sel[2] = tmp; + } + if(sel[1] > sel[3]) { + tmp = sel[1]; + sel[1] = sel[3]; + sel[3] = tmp; + } + + 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) { + var _this = this; + + 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) { + this.instance._registerTimeout(setTimeout(function () { + _this._fireCallbacks(true); + }, 0)); + + return; + } + + if (this.state == Handsontable.EditorState.EDITING) { + if (restoreOriginalValue) { + this.cancelChanges(); + this.instance.view.render(); + + 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)) { + this.instance.addHookOnce('postAfterValidate', function (result) { + _this.state = Handsontable.EditorState.FINISHED; + _this.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; + } + // validator was defined and failed + if (result === false && this.cellProperties.allowInvalid !== true) { + 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(){ + var that = this; + this.createElements(); + this.eventManager = new Handsontable.eventManager(this); + this.bindEvents(); + this.autoResize = autoResize(); + + this.instance.addHook('afterDestroy', function () { + that.destroy(); + }); + }; + + 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) + + Handsontable.Dom.enableImmediatePropagation(event); + + //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; + } + + that.autoResize.resize(String.fromCharCode(event.keyCode)); + }; + + + + TextEditor.prototype.open = function(){ + this.refreshDimensions(); //need it instantly, to prevent https://github.com/handsontable/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'); + + 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.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); + 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(); + var cssTransformOffset; + + // TODO: Refactor this to the new instance.getCell method (from #ply-59), after 0.12.1 is released + switch(editorSection) { + case 'top': + cssTransformOffset = Handsontable.Dom.getCssTransform(this.instance.view.wt.wtScrollbars.vertical.clone.wtTable.holder.parentNode); + break; + case 'left': + cssTransformOffset = Handsontable.Dom.getCssTransform(this.instance.view.wt.wtScrollbars.horizontal.clone.wtTable.holder.parentNode); + break; + case 'corner': + cssTransformOffset = Handsontable.Dom.getCssTransform(this.instance.view.wt.wtScrollbars.corner.clone.wtTable.holder.parentNode); + break; + } + + if (editTop < 0) { + editTop = 0; + } + if (editLeft < 0) { + editLeft = 0; + } + if (rowHeadersCount > 0 && parseInt(this.TD.style.borderTopWidth, 10) > 0) { + editTop += 1; + } + if (colHeadersCount > 0 && parseInt(this.TD.style.borderLeftWidth, 10) > 0) { + editLeft += 1; + } + + + if(cssTransformOffset && cssTransformOffset != -1) { + this.textareaParentStyle[cssTransformOffset[0]] = cssTransformOffset[1]; + } else { + Handsontable.Dom.resetCssTransform(this.textareaParentStyle); + } + + this.textareaParentStyle.top = editTop + 'px'; + this.textareaParentStyle.left = editLeft + 'px'; + + ///end prepare textarea position + + + var cellTopOffset = this.TD.offsetTop - this.instance.view.wt.wtScrollbars.vertical.getScrollPosition(), + cellLeftOffset = this.TD.offsetLeft - this.instance.view.wt.wtScrollbars.horizontal.getScrollPosition(); + + var width = Handsontable.Dom.innerWidth(this.TD) - 8 //$td.width() + , maxWidth = this.instance.view.maximumVisibleElementWidth(cellLeftOffset) - 10 //10 is TEXTAREAs border and padding + , height = Handsontable.Dom.outerHeight(this.TD) - 4 //$td.outerHeight() - 4 + , maxHeight = this.instance.view.maximumVisibleElementHeight(cellTopOffset) - 2; //10 is TEXTAREAs border and padding + + if (parseInt(this.TD.style.borderTopWidth, 10) > 0) { + height -= 1; + } + if (parseInt(this.TD.style.borderLeftWidth, 10) > 0) { + if (rowHeadersCount > 0) { + width -= 1; + } + } + + this.TEXTAREA.style.fontSize = Handsontable.Dom.getComputedStyle(this.TD).fontSize; + this.TEXTAREA.style.fontFamily = Handsontable.Dom.getComputedStyle(this.TD).fontFamily; + + this.autoResize.init(this.TEXTAREA, { + 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) + }, true); + + this.textareaParentStyle.display = 'block'; + }; + + TextEditor.prototype.bindEvents = function () { + var editor = this; + + this.eventManager.addEventListener(this.TEXTAREA, 'cut',function (event){ + Handsontable.helper.stopPropagation(event); + //event.stopPropagation(); + }); + + this.eventManager.addEventListener(this.TEXTAREA, 'paste', function (event){ + Handsontable.helper.stopPropagation(event); + //event.stopPropagation(); + }); + + this.instance.addHook('afterScrollVertically', function () { + editor.refreshDimensions(); + }); + + this.instance.addHook('afterColumnResize', function () { + editor.refreshDimensions(); + editor.focus(); + }); + + this.instance.addHook('afterRowResize', function () { + editor.refreshDimensions(); + editor.focus(); + }); + + this.instance.addHook('afterDestroy', function () { + editor.eventManager.clear(); + }); + }; + + TextEditor.prototype.destroy = function () { + this.eventManager.clear(); + }; + + + Handsontable.editors.TextEditor = TextEditor; + Handsontable.editors.registerEditor('text', Handsontable.editors.TextEditor); + +})(Handsontable); + +(function (Handsontable) { + + var MobileTextEditor = Handsontable.editors.BaseEditor.prototype.extend(); + + var domDimensionsCache = {}; + + var createControls = function () { + this.controls = {}; + + this.controls.leftButton = document.createElement('DIV'); + this.controls.leftButton.className = 'leftButton'; + this.controls.rightButton = document.createElement('DIV'); + this.controls.rightButton.className = 'rightButton'; + this.controls.upButton = document.createElement('DIV'); + this.controls.upButton.className = 'upButton'; + this.controls.downButton = document.createElement('DIV'); + this.controls.downButton.className = 'downButton'; + + for (var button in this.controls) { + if (this.controls.hasOwnProperty(button)) { + this.positionControls.appendChild(this.controls[button]); + } + } + }; + + MobileTextEditor.prototype.valueChanged = function () { + return this.initValue != this.getValue(); + }; + + MobileTextEditor.prototype.init = function () { + var that = this; + this.eventManager = new Handsontable.eventManager(this.instance); + + this.createElements(); + this.bindEvents(); + + this.instance.addHook('afterDestroy', function () { + that.destroy(); + }); + + }; + + MobileTextEditor.prototype.getValue = function () { + return this.TEXTAREA.value; + }; + + MobileTextEditor.prototype.setValue = function (newValue) { + this.initValue = newValue; + + this.TEXTAREA.value = newValue; + }; + + MobileTextEditor.prototype.createElements = function () { + this.editorContainer = document.createElement('DIV'); + this.editorContainer.className = "htMobileEditorContainer"; + + this.cellPointer = document.createElement('DIV'); + this.cellPointer.className = "cellPointer"; + + this.moveHandle = document.createElement('DIV'); + this.moveHandle.className = "moveHandle"; + + this.inputPane = document.createElement('DIV'); + this.inputPane.className = "inputs"; + + this.positionControls = document.createElement('DIV'); + this.positionControls.className = "positionControls"; + + this.TEXTAREA = document.createElement('TEXTAREA'); + Handsontable.Dom.addClass(this.TEXTAREA, 'handsontableInput'); + + this.inputPane.appendChild(this.TEXTAREA); + + this.editorContainer.appendChild(this.cellPointer); + this.editorContainer.appendChild(this.moveHandle); + this.editorContainer.appendChild(this.inputPane); + this.editorContainer.appendChild(this.positionControls); + + createControls.call(this); + + document.body.appendChild(this.editorContainer); + }; + + MobileTextEditor.prototype.onBeforeKeyDown = function (event) { + var instance = this; + var that = instance.getActiveEditor(); + + Handsontable.Dom.enableImmediatePropagation(event); + + if (event.target !== that.TEXTAREA || event.isImmediatePropagationStopped()){ + return; + } + + var keyCodes = Handsontable.helper.keyCode; + + switch(event.keyCode) { + case keyCodes.ENTER: + that.close(); + event.preventDefault(); //don't add newline to field + break; + case keyCodes.BACKSPACE: + event.stopImmediatePropagation(); //backspace, delete, home, end should only work locally when cell is edited (not in table context) + break; + } + }; + + MobileTextEditor.prototype.open = function () { + this.instance.addHook('beforeKeyDown', this.onBeforeKeyDown); + + Handsontable.Dom.addClass(this.editorContainer, 'active'); + //this.updateEditorDimensions(); + //this.scrollToView(); + Handsontable.Dom.removeClass(this.cellPointer, 'hidden'); + + this.updateEditorPosition(); + }; + + MobileTextEditor.prototype.focus = function(){ + this.TEXTAREA.focus(); + Handsontable.Dom.setCaretPosition(this.TEXTAREA, this.TEXTAREA.value.length); + }; + + MobileTextEditor.prototype.close = function () { + this.TEXTAREA.blur(); + this.instance.removeHook('beforeKeyDown', this.onBeforeKeyDown); + + Handsontable.Dom.removeClass(this.editorContainer, 'active'); + }; + + MobileTextEditor.prototype.scrollToView = function () { + var coords = this.instance.getSelectedRange().highlight; + this.instance.view.scrollViewport(coords); + }; + + MobileTextEditor.prototype.hideCellPointer = function () { + if(!Handsontable.Dom.hasClass(this.cellPointer, 'hidden')) { + Handsontable.Dom.addClass(this.cellPointer, 'hidden'); + } + }; + + MobileTextEditor.prototype.updateEditorPosition = function (x, y) { + if(x && y) { + x = parseInt(x, 10); + y = parseInt(y, 10); + + this.editorContainer.style.top = y + "px"; + this.editorContainer.style.left = x + "px"; + + } else { + var selection = this.instance.getSelected() + , selectedCell = this.instance.getCell(selection[0],selection[1]); + + //cache sizes + if(!domDimensionsCache.cellPointer) { + domDimensionsCache.cellPointer = { + height: Handsontable.Dom.outerHeight(this.cellPointer), + width: Handsontable.Dom.outerWidth(this.cellPointer) + }; + } + if(!domDimensionsCache.editorContainer) { + domDimensionsCache.editorContainer = { + width: Handsontable.Dom.outerWidth(this.editorContainer) + }; + } + + if(selectedCell !== undefined) { + var scrollLeft = this.instance.view.wt.wtScrollbars.horizontal.scrollHandler == window ? + 0 : Handsontable.Dom.getScrollLeft(this.instance.view.wt.wtScrollbars.horizontal.scrollHandler); + var scrollTop = this.instance.view.wt.wtScrollbars.vertical.scrollHandler == window ? + 0 : Handsontable.Dom.getScrollTop(this.instance.view.wt.wtScrollbars.vertical.scrollHandler); + + var selectedCellOffset = Handsontable.Dom.offset(selectedCell) + , selectedCellWidth = Handsontable.Dom.outerWidth(selectedCell) + , currentScrollPosition = { + x: scrollLeft, + y: scrollTop + }; + + this.editorContainer.style.top = parseInt(selectedCellOffset.top + Handsontable.Dom.outerHeight(selectedCell) - + currentScrollPosition.y + domDimensionsCache.cellPointer.height, 10) + "px"; + this.editorContainer.style.left = parseInt((window.innerWidth / 2) - + (domDimensionsCache.editorContainer.width / 2) ,10) + "px"; + + if(selectedCellOffset.left + selectedCellWidth / 2 > parseInt(this.editorContainer.style.left,10) + domDimensionsCache.editorContainer.width) { + this.editorContainer.style.left = window.innerWidth - domDimensionsCache.editorContainer.width + "px"; + } else if(selectedCellOffset.left + selectedCellWidth / 2 < parseInt(this.editorContainer.style.left,10) + 20) { + this.editorContainer.style.left = 0 + "px"; + } + + this.cellPointer.style.left = parseInt(selectedCellOffset.left - (domDimensionsCache.cellPointer.width / 2) - + Handsontable.Dom.offset(this.editorContainer).left + (selectedCellWidth / 2) - currentScrollPosition.x ,10) + "px"; + } + } + }; + + + // For the optional dont-affect-editor-by-zooming feature: + + //MobileTextEditor.prototype.updateEditorDimensions = function () { + // if(!this.beginningWindowWidth) { + // this.beginningWindowWidth = window.innerWidth; + // this.beginningEditorWidth = Handsontable.Dom.outerWidth(this.editorContainer); + // this.scaleRatio = this.beginningEditorWidth / this.beginningWindowWidth; + // + // this.editorContainer.style.width = this.beginningEditorWidth + "px"; + // return; + // } + // + // var currentScaleRatio = this.beginningEditorWidth / window.innerWidth; + // //if(currentScaleRatio > this.scaleRatio + 0.2 || currentScaleRatio < this.scaleRatio - 0.2) { + // if(currentScaleRatio != this.scaleRatio) { + // this.editorContainer.style["zoom"] = (1 - ((currentScaleRatio * this.scaleRatio) - this.scaleRatio)) * 100 + "%"; + // } + // + //}; + + MobileTextEditor.prototype.updateEditorData = function () { + var selected = this.instance.getSelected() + , selectedValue = this.instance.getDataAtCell(selected[0], selected[1]); + + this.row = selected[0]; + this.col = selected[1]; + this.setValue(selectedValue); + this.updateEditorPosition(); + }; + + MobileTextEditor.prototype.prepareAndSave = function () { + + if(!this.valueChanged()) { + return true; + } + + var val = [ + [String.prototype.trim.call(this.getValue())] + ]; + + this.saveValue(val); + }; + + MobileTextEditor.prototype.bindEvents = function () { + var that = this; + + this.eventManager.addEventListener(this.controls.leftButton, "touchend", function (event) { + that.prepareAndSave(); + that.instance.selection.transformStart(0, -1, null, true); + that.updateEditorData(); + event.preventDefault(); + }); + this.eventManager.addEventListener(this.controls.rightButton, "touchend", function (event) { + that.prepareAndSave(); + that.instance.selection.transformStart(0, 1, null, true); + that.updateEditorData(); + event.preventDefault(); + }); + this.eventManager.addEventListener(this.controls.upButton, "touchend", function (event) { + that.prepareAndSave(); + that.instance.selection.transformStart(-1, 0, null, true); + that.updateEditorData(); + event.preventDefault(); + }); + this.eventManager.addEventListener(this.controls.downButton, "touchend", function (event) { + that.prepareAndSave(); + that.instance.selection.transformStart(1, 0, null, true); + that.updateEditorData(); + event.preventDefault(); + }); + + this.eventManager.addEventListener(this.moveHandle, "touchstart", function (event) { + if (event.touches.length == 1) { + var touch = event.touches[0] + , onTouchPosition = { + x: that.editorContainer.offsetLeft, + y: that.editorContainer.offsetTop + } + , onTouchOffset = { + x: touch.pageX - onTouchPosition.x, + y: touch.pageY - onTouchPosition.y + }; + + that.eventManager.addEventListener(this, "touchmove", function (event) { + var touch = event.touches[0]; + that.updateEditorPosition(touch.pageX - onTouchOffset.x, touch.pageY - onTouchOffset.y); + that.hideCellPointer(); + event.preventDefault(); + }); + + } + }); + + this.eventManager.addEventListener(document.body, "touchend", function (event) { + if(!Handsontable.Dom.isChildOf(event.target, that.editorContainer) && !Handsontable.Dom.isChildOf(event.target, that.instance.rootElement)) { + that.close(); + } + }); + + this.eventManager.addEventListener(this.instance.view.wt.wtScrollbars.horizontal.scrollHandler, "scroll", function (event) { + if(that.instance.view.wt.wtScrollbars.horizontal.scrollHandler != window) { + that.hideCellPointer(); + } + }); + + this.eventManager.addEventListener(this.instance.view.wt.wtScrollbars.vertical.scrollHandler, "scroll", function (event) { + if(that.instance.view.wt.wtScrollbars.vertical.scrollHandler != window) { + that.hideCellPointer(); + } + }); + + }; + + MobileTextEditor.prototype.destroy = function () { + this.eventManager.clear(); + + this.editorContainer.parentNode.removeChild(this.editorContainer); + }; + + Handsontable.editors.MobileTextEditor = MobileTextEditor; + Handsontable.editors.registerEditor('mobile', Handsontable.editors.MobileTextEditor); + + + +})(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.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(); + + var $; + + DateEditor.prototype.init = function () { + if (typeof jQuery != 'undefined') { + $ = jQuery; + } else { + throw new Error("You need to include jQuery to your project in order to use the jQuery UI Datepicker."); + } + + 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); + + var eventManager = Handsontable.eventManager(this); + + /** + * Prevent recognizing clicking on jQuery Datepicker as clicking outside of table + */ + eventManager.addEventListener(this.datePicker, 'mousedown', function (event) { + Handsontable.helper.stopPropagation(event); + //event.stopPropagation(); + }); + + this.hideDatepicker(); + }; + + DateEditor.prototype.destroyElements = function () { + this.$datePicker.datepicker('destroy'); + this.$datePicker.remove(); + //var eventManager = Handsontable.eventManager(this); + //eventManager.removeEventListener(this.datePicker, 'mousedown'); + }; + + 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 offset = this.TD.getBoundingClientRect(), + DatepickerSettings, + datepickerSettings; + + this.datePickerStyle.top = (window.pageYOffset + offset.top + Handsontable.Dom.outerHeight(this.TD)) + 'px'; + this.datePickerStyle.left = (window.pageXOffset + offset.left) + 'px'; + + DatepickerSettings = function () {}; + DatepickerSettings.prototype = this.cellProperties; + datepickerSettings = new DatepickerSettings(); + datepickerSettings.defaultDate = this.originalValue || void 0; + this.$datePicker.datepicker('option', datepickerSettings); + + 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.htEditor = new Handsontable(DIV); + + this.assignHooks(); + }; + + 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(); + } + }; + + if (this.cellProperties.handsontable) { + Handsontable.helper.extend(options, cellProperties.handsontable); + } + if (this.htEditor) { + this.htEditor.destroy(); + } + + this.htEditor = new Handsontable(this.htContainer, options); + + //this.$htContainer.handsontable('destroy'); + //this.$htContainer.handsontable(options); + }; + + var onBeforeKeyDown = function (event) { + + if (event != null && event.isImmediatePropagationEnabled == null) { + event.stopImmediatePropagation = function () { + this.isImmediatePropagationEnabled = false; + this.cancelBubble = true; + }; + event.isImmediatePropagationEnabled = true; + event.isImmediatePropagationStopped = function () { + return !this.isImmediatePropagationEnabled; + }; + } + + if (event.isImmediatePropagationStopped()) { + return; + } + + var editor = this.getActiveEditor(); + + var innerHOT = editor.htEditor.getInstance(); //Handsontable.tmpHandsontable(editor.htContainer, 'getInstance'); + + var rowToSelect; + + if (event.keyCode == Handsontable.helper.keyCode.ARROW_DOWN) { + if (!innerHOT.getSelected()) { + rowToSelect = 0; + } + else { + var selectedRow = innerHOT.getSelected()[0]; + var lastRow = innerHOT.countRows() - 1; + rowToSelect = Math.min(lastRow, selectedRow + 1); + } + } + else if (event.keyCode == Handsontable.helper.keyCode.ARROW_UP) { + if (innerHOT.getSelected()) { + var selectedRow = innerHOT.getSelected()[0]; + rowToSelect = selectedRow - 1; + } + } + + if (rowToSelect !== void 0) { + if (rowToSelect < 0) { + innerHOT.deselectCell(); + } + else { + innerHOT.selectCell(rowToSelect, 0); + } + + event.preventDefault(); + event.stopImmediatePropagation(); + + editor.instance.listen(); + editor.TEXTAREA.focus(); + } + }; + + HandsontableEditor.prototype.open = function () { + + this.instance.addHook('beforeKeyDown', onBeforeKeyDown); + + Handsontable.editors.TextEditor.prototype.open.apply(this, arguments); + + //this.$htContainer.handsontable('render'); + + //Handsontable.tmpHandsontable(this.htContainer, 'render'); + this.htEditor.render(); + + if (this.cellProperties.strict) { + this.htEditor.selectCell(0,0); + this.TEXTAREA.style.visibility = 'hidden'; + } else { + this.htEditor.deselectCell(); + this.TEXTAREA.style.visibility = 'visible'; + } + + Handsontable.Dom.setCaretPosition(this.TEXTAREA, 0, this.TEXTAREA.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.htEditor.isListening()) { //if focus is still in the HOT editor + + //if (Handsontable.tmpHandsontable(this.htContainer,'isListening')) { //if focus is still in the HOT editor + //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.htEditor.getSelected()){ + //if (Handsontable.tmpHandsontable(this.htContainer,'getSelected')) { + //if (this.$htContainer.handsontable('getSelected')) { + // var value = this.$htContainer.handsontable('getInstance').getValue(); + var value = this.htEditor.getInstance().getValue(); + //var value = Handsontable.tmpHandsontable(this.htContainer,'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); + }; + + HandsontableEditor.prototype.assignHooks = function () { + var that = this; + this.instance.addHook('afterDestroy', function () { + if (that.htEditor) { + that.htEditor.destroy(); + } + }); + + }; + + 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); + + // set choices list initial height, so Walkontable can assign it's scroll handler + var choicesListHot = this.htEditor.getInstance(); + choicesListHot.updateSettings({ + height: 1 + }); + + this.query = null; + this.choices = []; + }; + + AutocompleteEditor.prototype.createElements = function(){ + Handsontable.editors.HandsontableEditor.prototype.createElements.apply(this, arguments); + + var getSystemSpecificPaddingClass = function () { + if(window.navigator.platform.indexOf('Mac') != -1) { + return "htMacScroll"; + } else { + return ""; + } + }; + + Handsontable.Dom.addClass(this.htContainer, 'autocompleteEditor'); + Handsontable.Dom.addClass(this.htContainer, getSystemSpecificPaddingClass()); + + }; + + var skipOne = false; + var onBeforeKeyDown = function (event) { + skipOne = false; + var editor = this.getActiveEditor(); + var keyCodes = Handsontable.helper.keyCode; + + if (Handsontable.helper.isPrintableChar(event.keyCode) || event.keyCode === keyCodes.BACKSPACE || event.keyCode === keyCodes.DELETE || event.keyCode === keyCodes.INSERT) { + var timeOffset = 0; + + // on ctl+c / cmd+c don't update suggestion list + if(event.keyCode === keyCodes.C && (event.ctrlKey || event.metaKey)) { + return; + } + + if(!editor.isOpened()) { + timeOffset += 10; + } + + editor.instance._registerTimeout(setTimeout(function () { + editor.queryChoices(editor.TEXTAREA.value); + skipOne = true; + }, timeOffset)); + } + }; + + AutocompleteEditor.prototype.prepare = function () { + this.instance.addHook('beforeKeyDown', onBeforeKeyDown); + Handsontable.editors.HandsontableEditor.prototype.prepare.apply(this, arguments); + }; + + AutocompleteEditor.prototype.open = function () { + Handsontable.editors.HandsontableEditor.prototype.open.apply(this, arguments); + + this.TEXTAREA.style.visibility = 'visible'; + this.focus(); + + this.htContainer.style.overflow = 'hidden'; // small hack to prevent vertical scrollbar causing a horizontal scrollbar + + var choicesListHot = this.htEditor.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; + + if(value){ + 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 + ''); + } + } + } + }); + + if(skipOne) { + skipOne = false; + } + that.instance._registerTimeout(setTimeout(function () { + that.queryChoices(that.TEXTAREA.value); + that.htContainer.style.overflow = 'auto'; // small hack to prevent vertical scrollbar causing a horizontal scrollbar + }, 0)); + + }; + + AutocompleteEditor.prototype.close = function () { + 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 (Array.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) { + var pos = Handsontable.Dom.getCaretPosition(this.TEXTAREA), + endPos = Handsontable.Dom.getSelectionEndPosition(this.TEXTAREA); + + var orderByRelevance = AutocompleteEditor.sortByRelevance(this.getValue(), choices, this.cellProperties.filteringCaseSensitive); + var highlightIndex; + + /* jshint ignore:start */ + if (this.cellProperties.filter != false) { + var sorted = []; + for(var i = 0, choicesCount = orderByRelevance.length; i < choicesCount; i++) { + sorted.push(choices[orderByRelevance[i]]); + } + highlightIndex = 0; + choices = sorted; + } + else { + highlightIndex = orderByRelevance[0]; + } + /* jshint ignore:end */ + + this.choices = choices; + + this.htEditor.loadData(Handsontable.helper.pivot([choices])); + this.htEditor.updateSettings({height: this.getDropdownHeight()}); + //Handsontable.tmpHandsontable(this.htContainer,'loadData', Handsontable.helper.pivot([choices])); + //Handsontable.tmpHandsontable(this.htContainer,'updateSettings', {height: this.getDropdownHeight()}); + + if (this.cellProperties.strict === true) { + this.highlightBestMatchingChoice(highlightIndex); + } + + this.instance.listen(); + this.TEXTAREA.focus(); + Handsontable.Dom.setCaretPosition(this.TEXTAREA, pos, (pos != endPos ? endPos : void 0)); + }; + + AutocompleteEditor.prototype.finishEditing = function (restoreOriginalValue) { + if (!restoreOriginalValue) { + this.instance.removeHook('beforeKeyDown', onBeforeKeyDown); + } + Handsontable.editors.HandsontableEditor.prototype.finishEditing.apply(this, arguments); + }; + + AutocompleteEditor.prototype.highlightBestMatchingChoice = function (index) { + if (typeof index === "number") { + this.htEditor.selectCell(index, 0); + } else { + this.htEditor.deselectCell(); + } + }; + + /** + * Filters and sorts by relevance + * @param value + * @param choices + * @param caseSensitive + * @returns {Array} array of indexes in original choices array + */ + AutocompleteEditor.sortByRelevance = function(value, choices, caseSensitive) { + + var choicesRelevance = [] + , currentItem + , valueLength = value.length + , valueIndex + , charsLeft + , result = [] + , i + , choicesCount; + + if(valueLength === 0) { + for(i = 0, choicesCount = choices.length; i < choicesCount; i++) { + result.push(i); + } + return result; + } + + for(i = 0, choicesCount = choices.length; i < choicesCount; i++) { + currentItem = choices[i]; + + if(caseSensitive) { + valueIndex = currentItem.indexOf(value); + } else { + valueIndex = currentItem.toLowerCase().indexOf(value.toLowerCase()); + } + + + if(valueIndex == -1) { continue; } + charsLeft = currentItem.length - valueIndex - valueLength; + + choicesRelevance.push({ + baseIndex: i, + index: valueIndex, + charsLeft: charsLeft, + value: currentItem + }); + } + + choicesRelevance.sort(function(a, b) { + + if(b.index === -1) { + return -1; + } + if(a.index === -1) { + return 1; + } + + if(a.index < b.index) { + return -1; + } else if(b.index < a.index) { + return 1; + } else if(a.index === b.index) { + if(a.charsLeft < b.charsLeft) { + return -1; + } else if(a.charsLeft > b.charsLeft) { + return 1; + } else { + return 0; + } + } + }); + + for(i = 0, choicesCount = choicesRelevance.length; i < choicesCount; i++) { + result.push(choicesRelevance[i].baseIndex); + } + + return result; + }; + + AutocompleteEditor.prototype.getDropdownHeight = function(){ + //var firstRowHeight = this.$htContainer.handsontable('getInstance').getRowHeight(0) || 23; + var firstRowHeight = this.htEditor.getInstance().getRowHeight(0) || 23; + //var firstRowHeight = Handsontable.tmpHandsontable(this.htContainer,'getInstance').getRowHeight(0) || 23; + return this.choices.length >= 10 ? 10 * firstRowHeight : this.choices.length * firstRowHeight + 8; + //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; + + 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.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 (Array.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; + } + }; + + // TODO: Refactor this with the use of new getCell() after 0.12.1 + SelectEditor.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'; + } + } + }; + + 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); + var tdOffset = Handsontable.Dom.offset(this.TD); + var editorSection = this.checkEditorSection(); + var cssTransformOffset; + + switch(editorSection) { + case 'top': + cssTransformOffset = Handsontable.Dom.getCssTransform(this.instance.view.wt.wtScrollbars.vertical.clone.wtTable.holder.parentNode); + break; + case 'left': + cssTransformOffset = Handsontable.Dom.getCssTransform(this.instance.view.wt.wtScrollbars.horizontal.clone.wtTable.holder.parentNode); + break; + case 'corner': + cssTransformOffset = Handsontable.Dom.getCssTransform(this.instance.view.wt.wtScrollbars.corner.clone.wtTable.holder.parentNode); + break; + } + + var selectStyle = this.select.style; + + if(cssTransformOffset && cssTransformOffset != -1) { + selectStyle[cssTransformOffset[0]] = cssTransformOffset[1]; + } else { + Handsontable.Dom.resetCssTransform(this.select); + } + + selectStyle.height = height + 'px'; + selectStyle.minWidth = width + 'px'; + selectStyle.top = tdOffset.top - rootOffset.top + 'px'; + selectStyle.left = tdOffset.left - rootOffset.left + 'px'; + selectStyle.margin = '0px'; + selectStyle.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); +(function (Handsontable) { + + 'use strict'; + + var NumericEditor = Handsontable.editors.TextEditor.prototype.extend(); + + NumericEditor.prototype.beginEditing = function (initialValue) { + + var BaseEditor = Handsontable.editors.TextEditor.prototype; + + if (typeof (initialValue) === 'undefined' && this.originalValue) { + + var value = '' + this.originalValue; + + if (typeof this.cellProperties.language !== 'undefined') { + numeral.language(this.cellProperties.language); + } + + var decimalDelimiter = numeral.languageData().delimiters.decimal; + value = value.replace('.', decimalDelimiter); + + BaseEditor.beginEditing.apply(this, [value]); + } else { + BaseEditor.beginEditing.apply(this, arguments); + } + + }; + + Handsontable.editors.NumericEditor = NumericEditor; + Handsontable.editors.registerEditor('numeric', NumericEditor); + +})(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 {*} callback - Callback called with validation result + */ +Handsontable.AutocompleteValidator = function (value, callback) { + if (this.strict && this.source) { + if ( typeof this.source === 'function' ) { + this.source(value, process(value, callback)); + } else { + process(value, callback)(this.source); + } + } else { + callback(true); + } +}; + +/** + * Cell type is just a shortcut for setting bunch of cellProperties (used in getCellMeta) + */ + +Handsontable.mobileBrowser = Handsontable.helper.isMobileBrowser(); // check if viewed on a mobile device + +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.mobileBrowser ? Handsontable.editors.MobileTextEditor : Handsontable.editors.TextEditor, + renderer: Handsontable.renderers.TextRenderer +}; + +Handsontable.NumericCell = { + editor: Handsontable.editors.NumericEditor, + 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 + }, + 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.removeEventListener) { + element.removeEventListener(event, handler, false); + } else { + element.detachEvent('on' + event, handler); + } + }, + resize = function (newChar) { + var width, scrollHeight; + + if (!newChar) { + newChar = ""; + } else if (!/^[a-zA-Z \.,\\\/\|0-9]$/.test(newChar)) { + newChar = "."; + } + + if (text.textContent !== void 0) { + text.textContent = el.value + newChar; + } + else { + text.data = el.value + newChar; //IE8 + } + span.style.fontSize = Handsontable.Dom.getComputedStyle(el).fontSize; + span.style.fontFamily = Handsontable.Dom.getComputedStyle(el).fontFamily; + span.style.whiteSpace = "pre"; + + body.appendChild(span); + width = span.clientWidth + 2; + 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'; + } + scrollHeight = el.scrollHeight ? el.scrollHeight - 1 : 0; + + 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(!span.firstChild) { + span.className = "autoResize"; + span.style.display = 'inline-block'; + span.appendChild(text); + } + }, + init = function (el_, config, doObserve) { + 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.overflowY = 'hidden'; + } + + if(doObserve) { + 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, doObserve) { + init(el_, config, doObserve); + }, + unObserve: function () { + unObserve(el, 'change', resize); + unObserve(el, 'cut', delayedResize); + unObserve(el, 'paste', delayedResize); + unObserve(el, 'drop', delayedResize); + unObserve(el, 'keydown', delayedResize); + }, + resize: resize + }; + +}; + +/** + * 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 = { + /** + * Decode spreadsheet string into array + * + * @param {String} str + * @returns {Array} + */ + 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; + }, + + /** + * Encode array into valid spreadsheet string + * + * @param arr + * @returns {String} + */ + stringify: function (arr) { + var r, rLen, c, cLen, str = '', val; + + for (r = 0, rLen = arr.length; r < rLen; r += 1) { + cLen = arr[r].length; + + for (c = 0; 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)); + +/** + * 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(); +} + +/** + * Initialize CopyPaste class + */ +CopyPasteClass.prototype.init = function () { + var + style, + parent; + + this.copyCallbacks = []; + this.cutCallbacks = []; + this.pasteCallbacks = []; + this._eventManager = Handsontable.eventManager(this); + + // 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'; + this.elTextarea.onpaste = function (event) { + if('WebkitAppearance' in document.documentElement.style) { // chrome and safari + this.value = event.clipboardData.getData("Text"); + + return false; + } + }; + 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; + } + } + this.keyDownRemoveEvent = this._eventManager.addEventListener(document.documentElement, 'keydown', this.onKeyDown.bind(this), false); +}; + +/** + * Call method on every key down event + * + * @param {DOMEvent} event + */ +CopyPasteClass.prototype.onKeyDown = function (event) { + var _this = this, + isCtrlDown = false; + + // mac + if (event.metaKey) { + isCtrlDown = true; + } + // pc + else if (event.ctrlKey && navigator.userAgent.indexOf('Mac') === -1) { + isCtrlDown = true; + } + if (isCtrlDown) { + // this is needed by fragmentSelection in Handsontable. Ignore copypaste.js behavior if fragment of cell text is selected + if (document.activeElement !== this.elTextarea && (this.getSelectionText() !== '' || + ['INPUT', 'SELECT', 'TEXTAREA'].indexOf(document.activeElement.nodeName) !== -1)) { + return; + } + + this.selectNodeText(this.elTextarea); + setTimeout(function () { + _this.selectNodeText(_this.elTextarea); + }, 0); + } + + /* 67 = c + * 86 = v + * 88 = x + */ + if (isCtrlDown && (event.keyCode === 67 || event.keyCode === 86 || event.keyCode === 88)) { + // that.selectNodeText(that.elTextarea); + + // works in all browsers, incl. Opera < 12.12 + if (event.keyCode === 88) { + setTimeout(function () { + _this.triggerCut(event); + }, 0); + } + else if (event.keyCode === 86) { + setTimeout(function () { + _this.triggerPaste(event); + }, 0); + } + } +}; + +//http://jsperf.com/textara-selection +//http://stackoverflow.com/questions/1502385/how-can-i-make-this-code-work-in-ie +/** + * Select all text contains in passed node element + * + * @param {Element} el + */ +CopyPasteClass.prototype.selectNodeText = function (el) { + if (el) { + el.select(); + } +}; + +//http://stackoverflow.com/questions/5379120/get-the-highlighted-selected-text +/** + * Get selection text + * + * @returns {String} + */ +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; +}; + +/** + * Make string copyable + * + * @param {String} str + */ +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); +};*/ + +/** + * Add function callback to onCut event + * + * @param {Function} fn + */ +CopyPasteClass.prototype.onCut = function (fn) { + this.cutCallbacks.push(fn); +}; + +/** + * Add function callback to onPaste event + * + * @param {Function} fn + */ +CopyPasteClass.prototype.onPaste = function (fn) { + this.pasteCallbacks.push(fn); +}; + +/** + * Remove callback from all events + * + * @param {Function} fn + * @returns {Boolean} + */ +CopyPasteClass.prototype.removeCallback = function (fn) { + var i, len; + + for (i = 0, len = this.copyCallbacks.length; i < len; i++) { + if (this.copyCallbacks[i] === fn) { + this.copyCallbacks.splice(i, 1); + + return true; + } + } + for (i = 0, len = this.cutCallbacks.length; i < len; i++) { + if (this.cutCallbacks[i] === fn) { + this.cutCallbacks.splice(i, 1); + + return true; + } + } + for (i = 0, len = this.pasteCallbacks.length; i < len; i++) { + if (this.pasteCallbacks[i] === fn) { + this.pasteCallbacks.splice(i, 1); + + return true; + } + } + + return false; +}; + +/** + * Trigger cut event + * + * @param {DOMEvent} event + */ +CopyPasteClass.prototype.triggerCut = function (event) { + var _this = this; + + if (_this.cutCallbacks) { + setTimeout(function () { + for (var i = 0, len = _this.cutCallbacks.length; i < len; i++) { + _this.cutCallbacks[i](event); + } + }, 50); + } +}; + +/** + * Trigger paste event + * + * @param {DOMEvent} event + * @param {String} str + */ +CopyPasteClass.prototype.triggerPaste = function (event, str) { + var _this = this; + + if (_this.pasteCallbacks) { + setTimeout(function () { + var val = str || _this.elTextarea.value; + + for (var i = 0, len = _this.pasteCallbacks.length; i < len; i++) { + _this.pasteCallbacks[i](val, event); + } + }, 50); + } +}; + +/** + * Destroy instance + */ +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.keyDownRemoveEvent(); + } +}; + +/** + * Check if instance has been destroyed + * + * @returns {Boolean} + */ +CopyPasteClass.prototype.hasBeenDestroyed = function () { + return !this.refCounter; +}; + + + +// 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); + /* jshint ignore:start */ + if (found != '') { + return escapePathComponent(key) + '/' + found; + } + /* jshint ignore:end */ + } + } + } + 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 = []; + /* jshint ignore:start */ + jsonpatch.intervals; + /* jshint ignore:end */ + 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; + /* jshint ignore:start */ + 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++; + } + /* jshint ignore:end */ + + 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: [], + beforeDrawBorders: [], + beforeChange: [], + beforeChangeRender: [], + beforeRemoveCol: [], + beforeRemoveRow: [], + beforeValidate: [], + beforeGetCellMeta: [], + beforeAutofill: [], + beforeKeyDown: [], + beforeOnCellMouseDown: [], + beforeTouchScroll: [], + 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: [], + afterIsMultipleSelectionCheck: [], + afterDocumentKeyDown: [], + afterMomentumScroll: [], + + // 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() { + /* jshint ignore:start */ + this.hooks = Hooks(); + /* jshint ignore:end */ + 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 (Array.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 been added (adding the same hook twice is now silently ignored) + } + } + return this; + }; + + PluginHookClass.prototype.once = function(key, fn, instance){ + + if(Array.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.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.run = 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._runBucket(this.globalBucket, instance, key, p1, p2, p3, p4, p5, p6); + p1 = this._runBucket(this.getBucket(instance), instance, key, p1, p2, p3, p4, p5, p6); + + return p1; + }; + + PluginHookClass.prototype._runBucket = function (bucket, instance, key, p1, p2, p3, p4, p5, p6) { + var handlers = bucket[key], + res, i, len; + + // performance considerations - http://jsperf.com/call-vs-apply-for-a-plugin-architecture + if (handlers) { + for (i = 0, len = handlers.length; i < len; 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); + } + } + } + } + + 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.className + ' htAutoColumnSize'; + tmp.table.className = instance.table.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.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.className + ' hidden'; +// 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; + + var eventManager = Handsontable.eventManager(instance); + eventManager.addEventListener(instance.rootElement, 'click', 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(typeof a[1] == "string") { + a[1] = a[1].toLowerCase(); + } + if(typeof b[1] == "string") { + b[1] = b[1].toLowerCase(); + } + + if (a[1] === b[1]) { + return 0; + } + if (a[1] === null || a[1] === "") { + return 1; + } + if (b[1] === null || b[1] === "") { + 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 && col >= 0) { + 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) { + /* jshint ignore:start */ + 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); + + } + + function align(range, type, alignment) { + /* jshint ignore:start */ + 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); + } + } + } + + this.render(); + + /* jshint ignore:end */ + } + + function ContextMenu(instance, customOptions) { + this.instance = instance; + var contextMenu = this; + contextMenu.menus = []; + contextMenu.htMenus = {}; + contextMenu.triggerRows = []; + + contextMenu.eventManager = Handsontable.eventManager(contextMenu); + + + this.enabled = true; + + this.instance.addHook('afterDestroy', function () { + contextMenu.destroy(); + }); + + this.defaultOptions = { + items: [ + { + key: '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.countRows() - 1, selected[1]], + columnSelected = entireColumnSelection.join(',') == selected.join(','); + + return selected[0] < 0 || this.countRows() >= this.getSettings().maxRows || columnSelected; + } + }, + { + key: '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.countRows() - 1, selected[1]], + columnSelected = entireColumnSelection.join(',') == selected.join(','); + + return this.getSelected()[0] < 0 || this.countRows() >= this.getSettings().maxRows || columnSelected; + } + }, + ContextMenu.SEPARATOR, + { + key: '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.countCols() - 1], + rowSelected = entireRowSelection.join(',') == selected.join(','); + + return this.getSelected()[1] < 0 || this.countCols() >= this.getSettings().maxCols || rowSelected; + } + }, + { + key: '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.countCols() - 1], + rowSelected = entireRowSelection.join(',') == selected.join(','); + + return selected[1] < 0 || this.countCols() >= this.getSettings().maxCols || rowSelected; + } + }, + ContextMenu.SEPARATOR, + { + key: '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.countRows() - 1, selected[1]], + columnSelected = entireColumnSelection.join(',') == selected.join(','); + return (selected[0] < 0 || columnSelected); + } + }, + { + key: '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.countCols() - 1], + rowSelected = entireRowSelection.join(',') == selected.join(','); + return (selected[1] < 0 || rowSelected); + } + }, + ContextMenu.SEPARATOR, + { + key: 'undo', + name: 'Undo', + callback: function () { + this.undo(); + }, + disabled: function () { + return this.undoRedo && !this.undoRedo.isUndoAvailable(); + } + }, + { + key: 'redo', + name: 'Redo', + callback: function () { + this.redo(); + }, + disabled: function () { + return this.undoRedo && !this.undoRedo.isRedoAvailable(); + } + }, + ContextMenu.SEPARATOR, + { + key: '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(); + } + }, + ContextMenu.SEPARATOR, + { + key: 'alignment', + name: 'Alignment', + submenu: { + items: [ + { + 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 + }, + { + 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 + }, + { + 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 + }, + { + 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 + }, + ContextMenu.SEPARATOR, + { + 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 + }, + { + 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 + }, + { + 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.options); + + this.bindMouseEvents(); + + this.markSelected = function (label) { + return "" + String.fromCharCode(10003) + "" + label; // workaround for https://github.com/handsontable/handsontable/issues/1946 + }; + + 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; + }; + + if(!this.instance.getSettings().allowInsertRow) { + var rowAboveIndex = findIndexByKey(this.defaultOptions.items, 'row_above'); + this.defaultOptions.items.splice(rowAboveIndex,1); + var rowBelowIndex = findIndexByKey(this.defaultOptions.items, 'row_above'); + this.defaultOptions.items.splice(rowBelowIndex,1); + this.defaultOptions.items.splice(rowBelowIndex,1); // FOR SEPARATOR + + } + + if(!this.instance.getSettings().allowInsertColumn) { + var colLeftIndex = findIndexByKey(this.defaultOptions.items, 'col_left'); + this.defaultOptions.items.splice(colLeftIndex,1); + var colRightIndex = findIndexByKey(this.defaultOptions.items, 'col_right'); + this.defaultOptions.items.splice(colRightIndex,1); + this.defaultOptions.items.splice(colRightIndex,1); // FOR SEPARATOR + + } + + var removeRow = false; + var removeCol = false; + var removeRowIndex, removeColumnIndex; + + if(!this.instance.getSettings().allowRemoveRow) { + removeRowIndex = findIndexByKey(this.defaultOptions.items, 'remove_row'); + this.defaultOptions.items.splice(removeRowIndex,1); + removeRow = true; + } + + if(!this.instance.getSettings().allowRemoveColumn) { + removeColumnIndex = findIndexByKey(this.defaultOptions.items, 'remove_col'); + this.defaultOptions.items.splice(removeColumnIndex,1); + removeCol = true; + } + + if (removeRow && removeCol) { + this.defaultOptions.items.splice(removeColumnIndex,1); // SEPARATOR + } + + 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); + + } + + /*** + * Create DOM instance of contextMenu + * @param menuName + * @param row + * @return {*} + */ + 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 = document.querySelector('.htContextMenu.' + menuName); + } else { + menu = document.querySelector('.htContextMenu'); + } + + + 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 () { + /* jshint ignore:start */ + function contextMenuOpenListener(event) { + var settings = this.instance.getSettings(); + + this.closeAll(); + + event.preventDefault(); + Handsontable.helper.stopPropagation(event); + + 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(settings.contextMenu); + + this.show(menu, items); + + this.setMenuPosition(event, menu); + + this.eventManager.addEventListener(document.documentElement, 'mousedown', Handsontable.helper.proxy(ContextMenu.prototype.closeAll, this)); + } + /* jshint ignore:end */ + var eventManager = Handsontable.eventManager(this.instance); + + eventManager.addEventListener(this.instance.rootElement, 'contextmenu', Handsontable.helper.proxy(contextMenuOpenListener, this)); + }; + + ContextMenu.prototype.bindTableEvents = function () { + this._afterScrollCallback = function () {}; + 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, hot) { + var contextMenu = this; + + 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(); + } + }; + + ContextMenu.prototype.unbindMouseEvents = function () { + this.eventManager.clear(); + var eventManager = Handsontable.eventManager(this.instance); + eventManager.removeEventListener(this.instance.rootElement, 'contextmenu'); + }; + + ContextMenu.prototype.show = function (menu, items) { + var that = this; + + menu.removeAttribute('style'); + menu.style.display = 'block'; + + var settings = { + data: items, + colHeaders: false, + colWidths: [200], + readOnly: true, + copyPaste: false, + columns: [ + { + data: 'name', + renderer: Handsontable.helper.proxy(this.renderer, this) + } + ], + renderAllRows: true, + beforeKeyDown: function (event) { + that.onBeforeKeyDown(event, htContextMenu); + }, + afterOnCellMouseOver: function (event, coords, TD) { + that.onCellMouseOver(event, coords, TD, htContextMenu); + } + }; + + var htContextMenu = new Handsontable(menu, settings); + + + this.eventManager.removeEventListener(menu, 'mousedown'); + this.eventManager.addEventListener(menu,'mousedown', function (event) { + that.performAction(event, htContextMenu); + }); + + this.bindTableEvents(); + htContextMenu.listen(); + + this.htMenus[htContextMenu.guid] = htContextMenu; + }; + + ContextMenu.prototype.close = function (menu) { + this.hide(menu); + this.eventManager.clear(); + 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); + } + + }; + + ContextMenu.prototype.hide = function (menu) { + menu.style.display = 'none'; + var instance =this.htMenus[menu.id]; + + instance.destroy(); + delete this.htMenus[menu.id]; + }; + + 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'); + + this.eventManager.addEventListener(wrapper, 'mouseenter', function () { + instance.deselectCell(); + }); + + } else { + if (isSubMenu(item)) { + Handsontable.Dom.addClass(TD, 'htSubmenu'); + + + this.eventManager.addEventListener(wrapper, 'mouseenter', function () { + instance.selectCell(row, col); + }); + + } else { + Handsontable.Dom.removeClass(TD, 'htSubmenu'); + Handsontable.Dom.removeClass(TD, 'htDisabled'); + + this.eventManager.addEventListener(wrapper, 'mouseenter', function () { + instance.selectCell(row, col); + }); + } + } + + + function isSubMenu(item) { + return item.hasOwnProperty('submenu'); + } + + function itemIsSeparator(item) { + return new RegExp(ContextMenu.SEPARATOR.name, '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, hot) { + var menusLength = this.menus.length; + + if (menusLength > 0) { + var lastMenu = this.menus[menusLength - 1]; + if (lastMenu.id != hot.guid) { + 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, instance) { + + Handsontable.Dom.enableImmediatePropagation(event); + var contextMenu = this; + + 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, instance); + } + 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 (instance.rootElement.className.indexOf('htContextSubMenu_') != -1) { + contextMenu.closeLastOpenedSubMenu(); + var index = contextMenu.menus.length; + + if (index > 0) { + var menu = contextMenu.menus[index - 1]; + + var triggerRow = contextMenu.triggerRows.pop(); + instance = this.htMenus[menu.id]; + 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(); + var subMenuInstance = contextMenu.show(subMenu, items); + + contextMenu.setSubMenuPosition(coords, subMenu); + subMenuInstance.selectCell(0, 0); + } + }; + + function findByKey(items, key) { + for (var i = 0, ilen = items.length; i < ilen; i++) { + if (items[i].key === key) { + return items[i]; + } + } + } + + function findIndexByKey(items, key) { + for (var i = 0, ilen = items.length; i < ilen; i++) { + if (items[i].key === key) { + return i; + } + } + } + + ContextMenu.prototype.getItems = function (items) { + var menu, item; + + function ContextMenuItem(rawItem) { + if (typeof rawItem == 'string') { + this.name = rawItem; + } else { + Handsontable.helper.extend(this, rawItem); + } + } + + ContextMenuItem.prototype = items; + + if (items && items.items) { + items = items.items; + } + + if (items === true) { + items = this.defaultOptions.items; + } + + if (1 == 1) { + menu = []; + for (var key in items) { + if (items.hasOwnProperty(key)) { + if (typeof items[key] === 'string') { + item = findByKey(this.defaultOptions.items, items[key]); + } + else { + item = findByKey(this.defaultOptions.items, key); + } + if (!item) { + item = items[key]; + } + item = new ContextMenuItem(item); + if (typeof items[key] === 'object') { + Handsontable.helper.extend(item, items[key]); + } + if (!item.key) { + item.key = key; + } + menu.push(item); + } + } + } + + return menu; + }; + + 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, document.body.clientWidth)) { + 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, document.body.clientWidth)) { + this.positionMenuOnRightOfCursor(cursor, menu, true); + } else { + this.positionMenuOnLeftOfCursor(cursor, menu, true); + } + }; + + ContextMenu.prototype.setMenuPosition = function (event, menu) { + // for ie8 + // http://msdn.microsoft.com/en-us/library/ie/ff974655(v=vs.85).aspx + var scrollTop = Handsontable.Dom.getWindowScrollTop(); + var scrollLeft = Handsontable.Dom.getWindowScrollLeft(); + var cursorY = event.pageY || (event.clientY + scrollTop); + var cursorX = event.pageX || (event.clientX + scrollLeft); + + 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, document.body.clientHeight)) { + this.positionMenuBelowCursor(cursor, menu); + } else { + if (this.menuFitsAboveCursor(cursor, menu)) { + this.positionMenuAboveCursor(cursor, menu); + } else { + this.positionMenuBelowCursor(cursor, menu); + } + } + + if (this.menuFitsOnRightOfCursor(cursor, menu, document.body.clientWidth)) { + 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, viewportHeight) { + return cursor.topRelative + menu.offsetHeight <= viewportHeight; + }; + + ContextMenu.prototype.menuFitsOnRightOfCursor = function (cursor, menu, viewportHeight) { + return cursor.leftRelative + menu.offsetWidth <= viewportHeight; + }; + + 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 = 1 + cursor.left + cursor.cellWidth + 'px'; + } else { + menu.style.left = 1 + 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.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 = document.querySelectorAll('.handsontable'); + var menuEnabled = false; + + for (var i = 0, len = hotContainers.length; i < len; i++) { + var instance = this.htMenus[hotContainers[i].id]; + if (instance && instance.getSettings().contextMenu) { + menuEnabled = true; + break; + } + } + + return menuEnabled; + }; + + ContextMenu.prototype.removeMenu = function (menu) { + if (menu.parentNode) { + this.menu.parentNode.removeChild(menu); + } + }; + + ContextMenu.SEPARATOR = {name: "---------"}; + + function updateHeight() { + /* jshint ignore:start */ + if (this.rootElement.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.name) { + realSeparatorHeight += 2; + } else { + realEntrySize += 26; + } + } + + this.view.wt.wtScrollbars.vertical.fixedContainer.style.height = realEntrySize + realSeparatorHeight + "px"; + /* jshint ignore:end */ + } + + function init() { + /* jshint ignore:start */ + var instance = this; + /* jshint ignore:end */ + 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(); + } 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); + + Handsontable.PluginHooks.register('afterContextMenuDefaultOptions'); + + Handsontable.ContextMenu = ContextMenu; + +})(Handsontable); + +function Comments(instance) { + + var eventManager = Handsontable.eventManager(instance), + 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) { + eventManager.removeEventListener(document, 'mouseover'); + if (!(event.target.className == 'htCommentTextArea' || event.target.innerHTML.indexOf('Comment') != -1)) { + var value = document.querySelector('.htCommentTextArea').value; + if (value.trim().length > 1) { + saveComment(range, value, instance); + } + unBindMouseEvent(); + hideCommentTextArea(); + } + } + + eventManager.addEventListener(document, 'mousedown',Handsontable.helper.proxy(commentsListener)); + }, + unBindMouseEvent = function () { + eventManager.removeEventListener(document, 'mousedown'); + eventManager.addEventListener(document, 'mousedown', 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.querySelector('.htComments'); + + 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.querySelector('.htCommentTextArea').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 () { + eventManager.addEventListener(document, 'mouseover', 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.push(Handsontable.ContextMenu.SEPARATOR); + + defaultOptions.items.push({ + key: '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.push({ + key: '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); + + +/** + * 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') + , eventManager = Handsontable.eventManager(this); + + 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.appendChild(handle); + } + } + + function refreshHandlePosition(TH, delta) { + var box = TH.getBoundingClientRect(); + var handleWidth = 6; + if (delta > 0) { + handle.style.left = (box.left + box.width - handleWidth) + 'px'; + } + else { + 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.appendChild(guide); + } + + function refreshGuidePosition(diff) { + guide.style.left = startOffset + diff + 'px'; + } + + function hideHandleAndGuide() { + Handsontable.Dom.removeClass(handle, 'active'); + Handsontable.Dom.removeClass(guide, 'active'); + } + + var checkColumnHeader = function (element) { + if (element.tagName != 'BODY') { + if (element.parentNode.tagName == 'THEAD') { + return true; + } else { + element = element.parentNode; + return checkColumnHeader(element); + } + } + return false; + }; + + var getTHFromTargetElement = function (element) { + if (element.tagName != 'TABLE') { + if (element.tagName == 'TH') { + return element; + } else { + return getTHFromTargetElement(element.parentNode); + } + } + return null; + }; + + var bindEvents = function () { + + var instance = this; + var pressed; + + eventManager.addEventListener(instance.rootElement,'mouseover',function (e) { + if (checkColumnHeader(e.target)){ + var th = getTHFromTargetElement(e.target); + if (th) { + if (pressed) { + var col = instance.view.wt.wtTable.getCoords(th).col; + if(col >= 0) { //not TH above row header + endCol = col; + refreshHandlePosition(e.target, endCol - startCol); + } + } + else { + setupHandlePosition.call(instance, th); + } + } + } + }); + + eventManager.addEventListener(instance.rootElement,'mousedown', function (e) { + if (Handsontable.Dom.hasClass(e.target, 'manualColumnMover')){ + startX = Handsontable.helper.pageX(e); + setupGuidePosition.call(instance); + pressed = instance; + + startCol = currentCol; + endCol = currentCol; + } + }); + + eventManager.addEventListener(window,'mousemove',function (e) { + if (pressed) { + refreshGuidePosition(Handsontable.helper.pageX(e) - startX); + } + }); + + + eventManager.addEventListener(window,'mouseup',function (e) { + if (pressed) { + hideHandleAndGuide(); + pressed = false; + + 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(){ + eventManager.clear(); + }; + + 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 (Array.isArray(initialManualColumnPositions)) { + this.manualColumnPositions = initialManualColumnPositions; + } else { + this.manualColumnPositions = []; + } + + if (source == 'afterInit') { + + // update plugin usages count for manualColumnPositions + if (typeof instance.manualColumnPositionsPluginUsages != 'undefined') { + instance.manualColumnPositionsPluginUsages.push('manualColumnMove'); + } else { + instance.manualColumnPositionsPluginUsages = ['manualColumnMove']; + } + + bindEvents.call(this); + if (this.manualColumnPositions.length > 0) { + this.forceFullRender = true; + this.render(); + } + } + + } else { + var pluginUsagesIndex = instance.manualColumnPositionsPluginUsages ? instance.manualColumnPositionsPluginUsages.indexOf('manualColumnMove') : -1; + if (pluginUsagesIndex > -1) { + unbindEvents.call(this); + this.manualColumnPositions = []; + instance.manualColumnPositionsPluginUsages[pluginUsagesIndex] = void 0; + } + } + }; + + 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; + }; + + // need to reconstruct manualcolpositions after removing columns + this.afterRemoveCol = function (index, amount) { + if (!this.getSettings().manualColumnMove) { + return; + } + + var rmindx, + colpos = this.manualColumnPositions; + + // We have removed columns, we also need to remove the indicies from manual column array + rmindx = colpos.splice(index, amount); + + // We need to remap manualColPositions so it remains constant linear from 0->ncols + colpos = colpos.map(function (colpos) { + var i, newpos = colpos; + + for (i = 0; i < rmindx.length; i++) { + if (colpos > rmindx[i]) { + newpos--; + } + } + + return newpos; + }); + + this.manualColumnPositions = colpos; + }; + + // need to reconstruct manualcolpositions after adding columns + this.afterCreateCol = function (index, amount) { + if (!this.getSettings().manualColumnMove) { + return; + } + + var colpos = this.manualColumnPositions; + if (!colpos.length) { + return; + } + + var addindx = []; + for (var i = 0; i < amount; i++) { + addindx.push(index + i); + } + + if (index >= colpos.length) { + colpos.concat(addindx); + } + else { + // We need to remap manualColPositions so it remains constant linear from 0->ncols + colpos = colpos.map(function (colpos) { + return (colpos >= index) ? (colpos + amount) : colpos; + }); + + // We have added columns, we also need to add new indicies to manualcolumn position array + colpos.splice.apply(colpos, [index, 0].concat(addindx)); + } + + this.manualColumnPositions = colpos; + }; +} +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.add('afterRemoveCol', htManualColumnMove.afterRemoveCol); +Handsontable.hooks.add('afterCreateCol', htManualColumnMove.afterCreateCol); +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') + , eventManager = Handsontable.eventManager(this); + + + 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.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.appendChild(guide); + } + + function refreshGuidePosition() { + guide.style.left = handle.style.left; + } + + function hideHandleAndGuide() { + Handsontable.Dom.removeClass(handle, 'active'); + Handsontable.Dom.removeClass(guide, 'active'); + } + + var checkColumnHeader = function (element) { + if (element.tagName != 'BODY') { + if (element.parentNode.tagName == 'THEAD') { + return true; + } else { + element = element.parentNode; + return checkColumnHeader(element); + } + } + return false; + }; + + var getTHFromTargetElement = function (element) { + if (element.tagName != 'TABLE') { + if (element.tagName == 'TH') { + return element; + } else { + return getTHFromTargetElement(element.parentNode); + } + } + return null; + }; + + var bindEvents = function () { + var instance = this; + var pressed; + var dblclick = 0; + var autoresizeTimeout = null; + + eventManager.addEventListener(instance.rootElement, 'mouseover', function (e) { + if (checkColumnHeader(e.target)) { + var th = getTHFromTargetElement(e.target); + if (th) { + if (!pressed) { + setupHandlePosition.call(instance, th); + } + } + } + }); + + eventManager.addEventListener(instance.rootElement, 'mousedown', function (e) { + if (Handsontable.Dom.hasClass(e.target, 'manualColumnResizer')) { + 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 = Handsontable.helper.pageX(e); + newSize = startWidth; + } + }); + + eventManager.addEventListener(window, 'mousemove', function (e) { + if (pressed) { + currentWidth = startWidth + (Handsontable.helper.pageX(e) - startX); + newSize = setManualSize(currentCol, currentWidth); //save col width + refreshHandlePosition(); + refreshGuidePosition(); + } + }); + + eventManager.addEventListener(window, 'mouseup', 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 () { + eventManager.clear(); + }; + + 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); + + // update plugin usages count for manualColumnPositions + if (typeof instance.manualColumnWidthsPluginUsages != 'undefined') { + instance.manualColumnWidthsPluginUsages.push('manualColumnResize'); + } else { + instance.manualColumnWidthsPluginUsages = ['manualColumnResize']; + } + + if (typeof loadedManualColumnWidths != 'undefined') { + this.manualColumnWidths = loadedManualColumnWidths; + } else if (Array.isArray(initialColumnWidths)) { + this.manualColumnWidths = initialColumnWidths; + } else { + this.manualColumnWidths = []; + } + + if (source == 'afterInit') { + bindEvents.call(this); + if (this.manualColumnWidths.length > 0) { + this.forceFullRender = true; + this.render(); + } + } + } + else { + var pluginUsagesIndex = instance.manualColumnWidthsPluginUsages ? instance.manualColumnWidthsPluginUsages.indexOf('manualColumnResize') : -1; + if (pluginUsagesIndex > -1) { + 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.run(instance, 'modifyCol', col); + instance.manualColumnWidths[col] = width; + + return width; + }; + + this.modifyColWidth = function (width, col) { + col = this.runHooks('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') + , eventManager = Handsontable.eventManager(this); + + 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.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.appendChild(guide); + } + + function refreshGuidePosition() { + guide.style.top = handle.style.top; + } + + function hideHandleAndGuide() { + Handsontable.Dom.removeClass(handle, 'active'); + Handsontable.Dom.removeClass(guide, 'active'); + } + + var checkRowHeader = function (element) { + if (element.tagName != 'BODY') { + if (element.parentNode.tagName == 'TBODY') { + return true; + } else { + element = element.parentNode; + return checkRowHeader(element); + } + } + return false; + }; + + var getTHFromTargetElement = function (element) { + if (element.tagName != 'TABLE') { + if (element.tagName == 'TH') { + return element; + } else { + return getTHFromTargetElement(element.parentNode); + } + } + return null; + }; + + var bindEvents = function () { + var instance = this; + var pressed; + var dblclick = 0; + var autoresizeTimeout = null; + + eventManager.addEventListener(instance.rootElement, 'mouseover', function (e) { + if (checkRowHeader(e.target)) { + var th = getTHFromTargetElement(e.target); + if (th) { + if (!pressed) { + setupHandlePosition.call(instance, th); + } + } + } + }); + + eventManager.addEventListener(instance.rootElement, 'mousedown', function (e) { + if (Handsontable.Dom.hasClass(e.target, 'manualRowResizer')) { + 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 = Handsontable.helper.pageY(e); + newSize = startHeight; + } + }); + + eventManager.addEventListener(window, 'mousemove', function (e) { + if (pressed) { + currentHeight = startHeight + (Handsontable.helper.pageY(e) - startY); + newSize = setManualSize(currentRow, currentHeight); + refreshHandlePosition(); + refreshGuidePosition(); + } + }); + + eventManager.addEventListener(window, 'mouseup', function (e) { + 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 () { + eventManager.clear(); + }; + + 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); + + // update plugin usages count for manualColumnPositions + if (typeof instance.manualRowHeightsPluginUsages != 'undefined') { + instance.manualRowHeightsPluginUsages.push('manualRowResize'); + } else { + instance.manualRowHeightsPluginUsages = ['manualRowResize']; + } + + if (typeof loadedManualRowHeights != 'undefined') { + this.manualRowHeights = loadedManualRowHeights; + } else if (Array.isArray(initialRowHeights)) { + 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 { + var pluginUsagesIndex = instance.manualRowHeightsPluginUsages ? instance.manualRowHeightsPluginUsages.indexOf('manualRowResize') : -1; + if (pluginUsagesIndex > -1) { + unbindEvents.call(this); + this.manualRowHeights = []; + instance.manualRowHeightsPluginUsages[pluginUsagesIndex] = void 0; + } + } + }; + + var setManualSize = function (row, height) { + row = Handsontable.hooks.run(instance, 'modifyRow', row); + instance.manualRowHeights[row] = height; + + return height; + }; + + this.modifyRowHeight = function (height, row) { + if (this.getSettings().manualRowResize) { + row = this.runHooks('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.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(Array.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 = Handsontable.helper.deepClone(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 = Handsontable.helper.deepClone(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 { + dragToScroll.setBoundaries(scrollHandler.getBoundingClientRect()); + } + + dragToScroll.setCallback(function (scrollX, scrollY) { + if (scrollX < 0) { + scrollHandler.scrollLeft -= 50; + } + else if (scrollX > 0) { + scrollHandler.scrollLeft += 50; + } + + if (scrollY < 0) { + scrollHandler.scrollTop -= 20; + } + else if (scrollY > 0) { + scrollHandler.scrollTop += 20; + } + }); + + instance.dragToScrollListening = true; + }; + + Handsontable.hooks.add('afterInit', function () { + var instance = this; + var eventManager = Handsontable.eventManager(this); + + eventManager.addEventListener(document,'mouseup', function () { + instance.dragToScrollListening = false; + }); + + eventManager.addEventListener(document,'mousemove', function (event) { + if (instance.dragToScrollListening) { + dragToScroll.check(event.clientX, event.clientY); + } + }); + }); + + Handsontable.hooks.add('afterDestroy', function () { + var eventManager = Handsontable.eventManager(this); + eventManager.clear(); + }); + + 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) { + var _this = this; + + this.copyPasteInstance = CopyPaste.getInstance(); + this.copyPasteInstance.onCut(onCut); + this.copyPasteInstance.onPaste(onPaste); + + instance.addHook('beforeKeyDown', onBeforeKeyDown); + + function onCut() { + if (!instance.isListening()) { + return; + } + instance.selection.empty(); + } + + function onPaste(str) { + var + input, + inputArray, + selected, + coordsFrom, + coordsTo, + cellRange, + topLeftCorner, + bottomRightCorner, + areaStart, + areaEnd; + + if (!instance.isListening() || !instance.selection.isSelected()) { + return; + } + input = str; + 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) { + var ctrlDown; + + if (instance.getSelected()) { + if (Handsontable.helper.isCtrlKey(event.keyCode)) { + // when CTRL is pressed, prepare selectable text in textarea + // http://stackoverflow.com/questions/3902635/how-does-one-capture-a-macs-command-key-via-javascript + _this.setCopyableText(); + event.stopImmediatePropagation(); + + return; + } + // catch CTRL but not right ALT (which in some systems triggers ALT+CTRL) + ctrlDown = (event.ctrlKey || event.metaKey) && !event.altKey; + + if (event.keyCode == Handsontable.helper.keyCode.A && ctrlDown) { + instance._registerTimeout(setTimeout(Handsontable.helper.proxy(_this.setCopyableText, _this), 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, + 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() { + /* jshint ignore:start */ + var instance = this; + /* jshint ignore:end */ + + 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 (Array.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.removeAttribute('rowspan'); + TD.removeAttribute('colspan'); + + TD.style.display = "none"; + } + } + else { + TD.removeAttribute('rowspan'); + TD.removeAttribute('colspan'); + } +}; + +MergeCells.prototype.modifyTransform = function (hook, currentSelectedRange, delta) { + var sameRowspan = function (merged, coords) { + if (coords.row >= merged.row && coords.row <= (merged.row + merged.rowspan - 1)) { + return true; + } + return false; + } + , sameColspan = function (merged, coords) { + if (coords.col >= merged.col && coords.col <= (merged.col + merged.colspan - 1)) { + return true; + } + return false; + } + , getNextPosition = function (newDelta) { + return new WalkontableCellCoords(currentSelectedRange.to.row + newDelta.row, currentSelectedRange.to.col + newDelta.col); + }; + + var newDelta = { + row: delta.row, + col: delta.col + }; + + + if (hook == 'modifyTransformStart') { + + if (!this.lastDesiredCoords) { + this.lastDesiredCoords = new WalkontableCellCoords(null, null); + } + var currentPosition = new WalkontableCellCoords(currentSelectedRange.highlight.row, currentSelectedRange.highlight.col) + , mergedParent = this.mergedCellInfoCollection.getInfo(currentPosition.row, currentPosition.col)// if current position's parent is a merged range, returns it + , currentRangeContainsMerge; // if current range contains a merged range + + for (var i = 0, mergesLength = this.mergedCellInfoCollection.length; i < mergesLength; i++) { + var range = this.mergedCellInfoCollection[i]; + range = new WalkontableCellCoords(range.row + range.rowspan - 1, range.col + range.colspan - 1); + if (currentSelectedRange.includes(range)) { + currentRangeContainsMerge = true; + break; + } + } + + if (mergedParent) { // only merge selected + var mergeTopLeft = new WalkontableCellCoords(mergedParent.row, mergedParent.col) + , mergeBottomRight = new WalkontableCellCoords(mergedParent.row + mergedParent.rowspan - 1, mergedParent.col + mergedParent.colspan - 1) + , mergeRange = new WalkontableCellRange(mergeTopLeft, mergeTopLeft, mergeBottomRight); + + if (!mergeRange.includes(this.lastDesiredCoords)) { + this.lastDesiredCoords = new WalkontableCellCoords(null, null); // reset outdated version of lastDesiredCoords + } + + newDelta.row = this.lastDesiredCoords.row ? this.lastDesiredCoords.row - currentPosition.row : newDelta.row; + newDelta.col = this.lastDesiredCoords.col ? this.lastDesiredCoords.col - currentPosition.col : newDelta.col; + + if (delta.row > 0) { // moving down + newDelta.row = mergedParent.row + mergedParent.rowspan - 1 - currentPosition.row + delta.row; + } else if (delta.row < 0) { //moving up + newDelta.row = currentPosition.row - mergedParent.row + delta.row; + } + if (delta.col > 0) { // moving right + newDelta.col = mergedParent.col + mergedParent.colspan - 1 - currentPosition.col + delta.col; + } else if (delta.col < 0) { // moving left + newDelta.col = currentPosition.col - mergedParent.col + delta.col; + } + } + + var nextPosition = new WalkontableCellCoords(currentSelectedRange.highlight.row + newDelta.row, currentSelectedRange.highlight.col + newDelta.col) + , nextParentIsMerged = this.mergedCellInfoCollection.getInfo(nextPosition.row, nextPosition.col); + + if (nextParentIsMerged) { // skipping the invisible cells in the merge range + this.lastDesiredCoords = nextPosition; + newDelta = { + row: nextParentIsMerged.row - currentPosition.row, + col: nextParentIsMerged.col - currentPosition.col + }; + } + } else if (hook == 'modifyTransformEnd') { + for (var i = 0, mergesLength = this.mergedCellInfoCollection.length; i < mergesLength; i++) { + var currentMerge = this.mergedCellInfoCollection[i] + , mergeTopLeft = new WalkontableCellCoords(currentMerge.row, currentMerge.col) + , mergeBottomRight = new WalkontableCellCoords(currentMerge.row + currentMerge.rowspan - 1, currentMerge.col + currentMerge.colspan - 1) + , mergedRange = new WalkontableCellRange(mergeTopLeft, mergeTopLeft, mergeBottomRight) + , sharedBorders = currentSelectedRange.getBordersSharedWith(mergedRange); + + if (mergedRange.isEqual(currentSelectedRange)) { // only the merged range is selected + currentSelectedRange.setDirection("NW-SE"); + } + else if (sharedBorders.length > 0) { + var mergeHighlighted = (currentSelectedRange.highlight.isEqual(mergedRange.from)); + + if (sharedBorders.indexOf('top') > -1) { // if range shares a border with the merged section, change range direction accordingly + if (currentSelectedRange.to.isSouthEastOf(mergedRange.from) && mergeHighlighted) { + currentSelectedRange.setDirection("NW-SE"); + } else if (currentSelectedRange.to.isSouthWestOf(mergedRange.from) && mergeHighlighted) { + currentSelectedRange.setDirection("NE-SW"); + } + } else if (sharedBorders.indexOf('bottom') > -1) { + if (currentSelectedRange.to.isNorthEastOf(mergedRange.from) && mergeHighlighted) { + currentSelectedRange.setDirection("SW-NE"); + } else if (currentSelectedRange.to.isNorthWestOf(mergedRange.from) && mergeHighlighted) { + currentSelectedRange.setDirection("SE-NW"); + } + } + } + + var nextPosition = getNextPosition(newDelta) + , withinRowspan = sameRowspan(currentMerge, nextPosition) + , withinColspan = sameColspan(currentMerge, nextPosition); + + if (currentSelectedRange.includesRange(mergedRange) && (mergedRange.includes(nextPosition) || withinRowspan || withinColspan)) { // if next step overlaps a merged range, jump past it + if (withinRowspan) { + if (newDelta.row < 0) { + newDelta.row -= currentMerge.rowspan - 1; + } else if (newDelta.row > 0) { + newDelta.row += currentMerge.rowspan - 1; + } + } + if (withinColspan) { + if (newDelta.col < 0) { + newDelta.col -= currentMerge.colspan - 1; + } else if (newDelta.col > 0) { + newDelta.col += currentMerge.colspan - 1; + } + } + } + } + } + + if (newDelta.row !== 0) { + delta.row = newDelta.row; + } + if (newDelta.col !== 0) { + delta.col = newDelta.col; + } +}; + +if (typeof Handsontable == 'undefined') { + throw new Error('Handsontable is not defined'); +} + +var beforeInit = function () { + var instance = this; + var mergeCellsSetting = instance.getSettings().mergeCells; + + if (mergeCellsSetting) { + if (!instance.mergeCells) { + instance.mergeCells = new MergeCells(mergeCellsSetting); + } + } +}; + +var afterInit = function () { + var instance = this; + if (instance.mergeCells) { + /** + * Monkey patch WalkontableTable.prototype.getCell to return TD for merged cell parent if asked for TD of a cell that is + * invisible due to the merge. This is not the cleanest solution but there is a test case for it (merged cells scroll) so feel free to refactor it! + */ + instance.view.wt.wtTable.getCell = function (coords) { + if (instance.getSettings().mergeCells) { + var mergeParent = instance.mergeCells.mergedCellInfoCollection.getInfo(coords.row, coords.col); + if (mergeParent) { + coords = mergeParent; + } + } + return WalkontableTable.prototype.getCell.call(this, coords); + }; + } +}; + +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.push(Handsontable.ContextMenu.SEPARATOR); + + defaultOptions.items.push({ + key: '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; + + var rangeExpanded = false; + do { + rangeExpanded = false; + + 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)) { + coords.row = selRange.to.row; + coords.col = selRange.to.col; + + rangeExpanded = true; + } + } + } while (rangeExpanded); + + } +}; + +/** + * Returns correct coordinates for merged start / end cells in selection for area borders + * @param corners + * @param className + */ +var beforeDrawAreaBorders = function (corners, className) { + if (className && className == 'area') { + var mergeCellsSetting = this.getSettings().mergeCells; + if (mergeCellsSetting) { + var selRange = this.getSelectedRange(); + var startRange = new WalkontableCellRange(selRange.from, selRange.from, selRange.from); + var stopRange = new WalkontableCellRange(selRange.to, selRange.to, selRange.to); + + 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 (startRange.expandByRange(mergedCellRange)) { + corners[0] = startRange.from.row; + corners[1] = startRange.from.col; + } + + if (stopRange.expandByRange(mergedCellRange)) { + corners[2] = stopRange.from.row; + corners[3] = stopRange.from.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; + } + } +}; + +var afterViewportRowCalculatorOverride = function (calc) { + var mergeCellsSetting = this.getSettings().mergeCells; + if (mergeCellsSetting) { + var colCount = this.countCols(); + var mergeParent; + for (var c = 0; c < colCount; c++) { + mergeParent = this.mergeCells.mergedCellInfoCollection.getInfo(calc.startRow, c); + if (mergeParent) { + if (mergeParent.row < calc.startRow) { + calc.startRow = mergeParent.row; + return afterViewportRowCalculatorOverride.call(this, calc); //recursively search upwards + } + } + mergeParent = this.mergeCells.mergedCellInfoCollection.getInfo(calc.endRow, c); + if (mergeParent) { + var mergeEnd = mergeParent.row + mergeParent.rowspan - 1; + if (mergeEnd > calc.endRow) { + calc.endRow = mergeEnd; + return afterViewportRowCalculatorOverride.call(this, calc); //recursively search upwards + } + } + } + } +}; + +var afterViewportColumnCalculatorOverride = function (calc) { + var mergeCellsSetting = this.getSettings().mergeCells; + if (mergeCellsSetting) { + var rowCount = this.countRows(); + var mergeParent; + for (var r = 0; r < rowCount; r++) { + mergeParent = this.mergeCells.mergedCellInfoCollection.getInfo(r, calc.startColumn); + + if (mergeParent) { + if (mergeParent.col < calc.startColumn) { + calc.startColumn = mergeParent.col; + return afterViewportColumnCalculatorOverride.call(this, calc); //recursively search upwards + } + } + mergeParent = this.mergeCells.mergedCellInfoCollection.getInfo(r, calc.endColumn); + if (mergeParent) { + var mergeEnd = mergeParent.col + mergeParent.colspan - 1; + if (mergeEnd > calc.endColumn) { + calc.endColumn = mergeEnd; + return afterViewportColumnCalculatorOverride.call(this, calc); //recursively search upwards + } + } + } + } +}; + +var isMultipleSelection = function (isMultiple) { + if (isMultiple && this.mergeCells) { + var mergedCells = this.mergeCells.mergedCellInfoCollection + , selectionRange = this.getSelectedRange(); + + for (var group in mergedCells) { + if (selectionRange.highlight.row == mergedCells[group].row && selectionRange.highlight.col == mergedCells[group].col && + selectionRange.to.row == mergedCells[group].row + mergedCells[group].rowspan - 1 && + selectionRange.to.col == mergedCells[group].col + mergedCells[group].colspan - 1) { + return false; + } + } + } + return isMultiple; +}; + +Handsontable.hooks.add('beforeInit', beforeInit); +Handsontable.hooks.add('afterInit', afterInit); +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('beforeDrawBorders', beforeDrawAreaBorders); +Handsontable.hooks.add('afterIsMultipleSelection', isMultipleSelection); +Handsontable.hooks.add('afterRenderer', afterRenderer); +Handsontable.hooks.add('afterContextMenuDefaultOptions', addMergeActionsToContextMenu); +Handsontable.hooks.add('afterGetCellMeta', afterGetCellMeta); +Handsontable.hooks.add('afterViewportRowCalculatorOverride', afterViewportRowCalculatorOverride); +Handsontable.hooks.add('afterViewportColumnCalculatorOverride', afterViewportColumnCalculatorOverride); + +Handsontable.MergeCells = MergeCells; + + +(function () { + + function CustomBorders () { + + } + +// /*** +// * Array for all custom border objects (for redraw) +// * @type {{}} +// */ +// var bordersArray = {}, + /*** + * Current instance (table where borders should be placed) + */ + var instance; + + + /*** + * Check if plugin should be enabled + */ + var checkEnable = function (customBorders) { + if(typeof customBorders === "boolean"){ + if (customBorders === true){ + return true; + } + } + + if(typeof customBorders === "object"){ + if(customBorders.length > 0) { + return true; + } + } + return false; + }; + + + /*** + * Initialize plugin + */ + var init = function () { + + if(checkEnable(this.getSettings().customBorders)){ + if(!this.customBorders){ + instance = this; + this.customBorders = new CustomBorders(); + } + } + }; + + /*** + * get index of border setting + * @param className + * @returns {number} + */ + var getSettingIndex = function (className) { + for (var i = 0; i < instance.view.wt.selections.length; i++){ + if (instance.view.wt.selections[i].settings.className == className){ + return i; + } + } + return -1; + }; + + /*** + * Insert WalkontableSelection instance into Walkontable.settings + * @param border + */ + var insertBorderIntoSettings = function (border) { + var coordinates = { + row: border.row, + col: border.col + }; + var selection = new WalkontableSelection(border, new WalkontableCellRange(coordinates, coordinates, coordinates)); + var index = getSettingIndex(border.className); + + if(index >=0) { + instance.view.wt.selections[index] = selection; + } else { + instance.view.wt.selections.push(selection); + } + }; + + /*** + * 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); + + insertBorderIntoSettings(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); + insertBorderIntoSettings(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; + }; + + /*** + * Remove borders divs from DOM + * + * @param borderClassName + */ + var removeBordersFromDom = function (borderClassName) { + var borders = document.querySelectorAll("." + borderClassName); + + for(var i = 0; i< borders.length; i++) { + if (borders[i]) { + if(borders[i].nodeName != 'TD') { + var parent = borders[i].parentNode; + if(parent.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); + this.removeCellMeta(row, col, 'borders'); + }; + + /*** + * 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; + /* jshint ignore:start */ + if (!bordersMeta || bordersMeta.border == undefined){ + bordersMeta = createEmptyBorders(row, col); + } + /* jshint ignore:end */ + + if (remove) { + bordersMeta[place] = createSingleEmptyBorder(); + } else { + bordersMeta[place] = createDefaultCustomBorder(); + } + + this.setCellMeta(row, col, 'borders', bordersMeta); + + var borderClassName = createClassName(row,col); + removeBordersFromDom(borderClassName); + insertBorderIntoSettings(bordersMeta); + + 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 "" + String.fromCharCode(10003) + "" + label; // workaround for https://github.com/handsontable/handsontable/issues/1946 + }; + + /*** + * Add border options to context menu + * + * @param defaultOptions + */ + var addBordersOptionsToContextMenu = function (defaultOptions) { + if(!this.getSettings().customBorders){ + return; + } + + defaultOptions.items.push(Handsontable.ContextMenu.SEPARATOR); + + defaultOptions.items.push({ + key: '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, 'left'); + 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('afterInit', function () { + var customBorders = this.getSettings().customBorders; + + if (customBorders){ + + 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]); + } + } + + this.render(); + this.view.wt.draw(true); + } + + }); + + 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'), + eventManager = Handsontable.eventManager(this); + + 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.appendChild(handle); + } + } + + function refreshHandlePosition(TH, delta) { + var box = TH.getBoundingClientRect(); + var handleHeight = 6; + if (delta > 0) { + handle.style.top = (box.top + box.height - handleHeight) + 'px'; + } + else { + 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.appendChild(guide); + } + + function refreshGuidePosition(diff) { + guide.style.top = startOffset + diff + 'px'; + } + + function hideHandleAndGuide() { + Handsontable.Dom.removeClass(handle, 'active'); + Handsontable.Dom.removeClass(guide, 'active'); + } + + var checkRowHeader = function (element) { + if (element.tagName != 'BODY') { + if (element.parentNode.tagName == 'TBODY') { + return true; + } else { + element = element.parentNode; + return checkRowHeader(element); + } + } + return false; + }; + + var getTHFromTargetElement = function (element) { + if (element.tagName != 'TABLE') { + if (element.tagName == 'TH') { + return element; + } else { + return getTHFromTargetElement(element.parentNode); + } + } + return null; + }; + + var bindEvents = function () { + var instance = this; + var pressed; + + + eventManager.addEventListener(instance.rootElement, 'mouseover', function (e) { + if (checkRowHeader(e.target)) { + var th = getTHFromTargetElement(e.target); + if (th) { + if (pressed) { + endRow = instance.view.wt.wtTable.getCoords(th).row; + refreshHandlePosition(th, endRow - startRow); + } + else { + setupHandlePosition.call(instance, th); + } + } + } + }); + + eventManager.addEventListener(instance.rootElement, 'mousedown', function (e) { + if (Handsontable.Dom.hasClass(e.target, 'manualRowMover')) { + startY = Handsontable.helper.pageY(e); + setupGuidePosition.call(instance); + pressed = instance; + + startRow = currentRow; + endRow = currentRow; + } + }); + + eventManager.addEventListener(window, 'mousemove', function (e) { + if (pressed) { + refreshGuidePosition(Handsontable.helper.pageY(e) - startY); + } + }); + + eventManager.addEventListener(window, 'mouseup', function (e) { + if (pressed) { + hideHandleAndGuide(); + pressed = false; + + 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 () { + eventManager.clear(); + }; + + 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); + + // update plugin usages count for manualColumnPositions + if (typeof instance.manualRowPositionsPluginUsages != 'undefined') { + instance.manualRowPositionsPluginUsages.push('manualColumnMove'); + } else { + instance.manualRowPositionsPluginUsages = ['manualColumnMove']; + } + + if (typeof loadedManualRowPostions != 'undefined') { + this.manualRowPositions = loadedManualRowPostions; + } else if (Array.isArray(initialManualRowPositions)) { + this.manualRowPositions = initialManualRowPositions; + } else { + this.manualRowPositions = []; + } + + if (source === 'afterInit') { + bindEvents.call(this); + if (this.manualRowPositions.length > 0) { + instance.forceFullRender = true; + instance.render(); + } + } + } else { + var pluginUsagesIndex = instance.manualRowPositionsPluginUsages ? instance.manualRowPositionsPluginUsages.indexOf('manualColumnMove') : -1; + if (pluginUsagesIndex > -1) { + unbindEvents.call(this); + instance.manualRowPositions = []; + instance.manualRowPositionsPluginUsages[pluginUsagesIndex] = void 0; + } + } + + }; + + 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 wtOnCellCornerMouseDown, + wtOnCellMouseOver, + mouseDownOnCellCorner = false, + plugin = this, + eventManager = Handsontable.eventManager(instance); + + + var mouseUpCallback = function (event) { + if (!instance.autofill) { + return true; + } + + if (instance.autofill.handle && instance.autofill.handle.isDragged) { + if (instance.autofill.handle.isDragged > 1) { + instance.autofill.apply(); + } + instance.autofill.handle.isDragged = 0; + mouseDownOnCellCorner = false; + } + }; + + eventManager.addEventListener(document, 'mouseup', function (event) { + mouseUpCallback(event); + }); + + eventManager.addEventListener(document,'mousemove', function (event){ + if (!plugin.instance.autofill) { + return 0; + } + + var tableBottom = Handsontable.Dom.offset(plugin.instance.table).top - (window.pageYOffset || document.documentElement.scrollTop) + Handsontable.Dom.outerHeight(plugin.instance.table) + , tableRight = Handsontable.Dom.offset(plugin.instance.table).left - (window.pageXOffset || document.documentElement.scrollLeft) + Handsontable.Dom.outerWidth(plugin.instance.table); + + + if (plugin.addingStarted === false && plugin.instance.autofill.handle.isDragged > 0 && event.clientY > tableBottom && event.clientX <= tableRight) { // dragged outside bottom + this.mouseDragOutside = true; + plugin.addingStarted = true; + } else { + this.mouseDragOutside = false; + } + + if (this.mouseDragOutside) { + setTimeout(function () { + plugin.addingStarted = false; + plugin.instance.alter('insert_row'); + }, 200); + } + }); + + /* + * 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; + mouseDownOnCellCorner = true; + + 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 && (mouseDownOnCellCorner && !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; + } + + var getDeltas = function (start, end, data, direction) { + var rlength = data.length, // rows + clength = data ? data[0].length : 0; // cols + + var deltas = []; + + var diffRow = end.row - start.row, + diffCol = end.col - start.col; + + var startValue, endValue, delta; + + var arr = []; + + if (['down', 'up'].indexOf(direction) !== -1) { + for (var col = 0; col <= diffCol; col++) { + + startValue = parseInt(data[0][col], 10); + endValue = parseInt(data[rlength-1][col], 10); + delta = (direction === 'down' ? (endValue - startValue) : (startValue - endValue)) / (rlength - 1) || 0; + + arr.push(delta); + } + + deltas.push(arr); + } + + if (['right', 'left'].indexOf(direction) !== -1) { + for (var row = 0; row <= diffRow; row++) { + + startValue = parseInt(data[row][0], 10); + endValue = parseInt(data[row][clength-1], 10); + delta = (direction === 'right' ? (endValue - startValue) : (startValue - endValue)) / (clength - 1) || 0; + + arr = []; + arr.push(delta); + + deltas.push(arr); + } + } + + return deltas; + }; + + 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(); + } + + var direction; + + if (drag[0] === select[0] && drag[1] < select[1]) { + direction = 'left'; + + 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]) { + direction = 'right'; + + 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]) { + direction = 'up'; + + 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]) { + direction = 'down'; + + start = new WalkontableCellCoords( + select[2] + 1, + drag[1] + ); + end = new WalkontableCellCoords( + drag[2], + drag[3] + ); + } + + if (start && start.row > -1 && start.col > -1) { + 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); + + var deltas = getDeltas(start, end, _data, direction); + + Handsontable.hooks.run(this.instance, 'beforeAutofill', start, end, _data); + + this.instance.populateFromArray(start.row, start.col, _data, end.row, end.col, 'autofill', null, direction, deltas); + + 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 + this.instance.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, + selection, + tableRows = this.instance.countRows(), + that = this; + + if (this.instance.view.wt.selections.fill.cellRange && this.addingStarted === false) { + selection = this.instance.getSelected(); + fillCorners = this.instance.view.wt.selections.fill.getCorners(); + + if (selection[2] < tableRows - 1 && 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); + +var Grouping = function (instance) { + /** + * array of items + * @type {Array} + */ + var groups = []; + + /** + * group definition + * @type {{id: String, level: Number, rows: Array, cols: Array, hidden: Number}} + */ + var item = { + id: '', + level: 0, + hidden: 0, + rows: [], + cols: [] + }; + + /** + * total rows and cols merged in groups + * @type {{rows: number, cols: number}} + */ + var counters = { + rows: 0, + cols: 0 + }; + + /** + * Number of group levels in each dimension + * @type {{rows: number, cols: number}} + */ + var levels = { + rows: 0, + cols: 0 + }; + + /** + * List of hidden rows + * @type {Array} + */ + var hiddenRows = []; + + /** + * List of hidden columns + * @type {Array} + */ + var hiddenCols = []; + + /** + * Classes used + */ + var classes = { + 'groupIndicatorContainer': 'htGroupIndicatorContainer', + 'groupIndicator': function (direction) { + return 'ht' + direction + 'Group'; + }, + 'groupStart': 'htGroupStart', + 'collapseButton': 'htCollapseButton', + 'expandButton': 'htExpandButton', + 'collapseGroupId': function (id) { + return 'htCollapse-' + id; + }, + 'collapseFromLevel': function (direction, level) { + return 'htCollapse' + direction + 'FromLevel-' + level; + }, + 'clickable': 'clickable', + 'levelTrigger': 'htGroupLevelTrigger' + }; + + /** + * compare object properties + * @param {String} property + * @param {String} orderDirection + * @returns {Function} + */ + var compare = function (property, orderDirection) { + return function (item1, item2) { + return typeof (orderDirection) === 'undefined' || orderDirection === 'asc' ? item1[property] - item2[property] : item2[property] - item1[property]; + }; + }; + + /** + * Create range array between from and to + * @param {Number} from + * @param {Number} to + * @returns {Array} + */ + var range = function (from, to) { + var arr = []; + while (from <= to) { + arr.push(from++); + } + + return arr; + }; + + /** + * * Get groups for range + * @param from + * @param to + * @returns {{total: {rows: number, cols: number}, groups: Array}} + */ + var getRangeGroups = function (dataType, from, to) { + var cells = [], + cell = { + row: null, + col: null + }; + + if (dataType == "cols") { + // get all rows for selected columns + while (from <= to) { + cell = { + row: -1, + col: from++ + }; + cells.push(cell); + } + + } else { + // get all columns for selected rows + while (from <= to) { + cell = { + row: from++, + col: -1 + }; + cells.push(cell); + } + } + + var cellsGroups = getCellsGroups(cells), + totalRows = 0, + totalCols = 0; + + // for selected cells, calculate total groups divided into rows and columns + for (var i = 0; i < cellsGroups.length; i++) { + totalRows += cellsGroups[i].filter(function (item) { + return item['rows']; + }).length; + + totalCols += cellsGroups[i].filter(function (item) { + return item['cols']; + }).length; + } + + return { + total: { + rows: totalRows, + cols: totalCols + }, + groups: cellsGroups + }; + }; + + /** + * Get all groups for cells + * @param {Array} cells [{row:0, col:0}, {row:0, col:1}, {row:1, col:2}] + * @returns {Array} + */ + var getCellsGroups = function (cells) { + var _groups = []; + + for (var i = 0; i < cells.length; i++) { + _groups.push(getCellGroups(cells[i])); + } + + return _groups; + }; + + /** + * Get all groups for cell + * @param {Object} coords {row:1, col:2} + * @param {Number} groupLevel Optional + * @param {String} groupType Optional + * @returns {Array} + */ + var getCellGroups = function (coords, groupLevel, groupType) { + var row = coords.row, + col = coords.col; + + // for row = -1 and col = -1, get all columns and rows + var tmpRow = (row === -1 ? 0 : row), + tmpCol = (col === -1 ? 0 : col); + + var _groups = []; + + for (var i = 0; i < groups.length; i++) { + var group = groups[i], + id = group['id'], + level = group['level'], + rows = group['rows'] || [], + cols = group['cols'] || []; + + if (_groups.indexOf(id) === -1) { + if (rows.indexOf(tmpRow) !== -1 || cols.indexOf(tmpCol) !== -1) { + _groups.push(group); + } + } + } + + // add col groups + if (col === -1) { + _groups = _groups.concat(getColGroups()); + } else if (row === -1) { + // add row groups + _groups = _groups.concat(getRowGroups()); + } + + if (groupLevel) { + _groups = _groups.filter(function (item) { + return item['level'] === groupLevel; + }); + } + + if (groupType) { + if (groupType === 'cols') { + _groups = _groups.filter(function (item) { + return item['cols']; + }); + } else if (groupType === 'rows') { + _groups = _groups.filter(function (item) { + return item['rows']; + }); + } + } + + // remove duplicates + var tmp = []; + return _groups.filter(function (item) { + if (tmp.indexOf(item.id) === -1) { + tmp.push(item.id); + return item; + } + }); + }; + + /** + * get group by id + * @param id + * @returns {Object} group + */ + var getGroupById = function (id) { + for (var i = 0, groupsLength = groups.length; i < groupsLength; i++) { + if (groups[i].id == id) { + return groups[i]; + } + } + return false; + }; + + /** + * get group by row and level + * @param row + * @param level + * @returns {Object} group + */ + var getGroupByRowAndLevel = function (row, level) { + for (var i = 0, groupsLength = groups.length; i < groupsLength; i++) { + if (groups[i].level == level && groups[i].rows && groups[i].rows.indexOf(row) > -1) { + return groups[i]; + } + } + return false; + }; + + /** + * get group by row and level + * @param row + * @param level + * @returns {Object} group + */ + var getGroupByColAndLevel = function (col, level) { + for (var i = 0, groupsLength = groups.length; i < groupsLength; i++) { + if (groups[i].level == level && groups[i].cols && groups[i].cols.indexOf(col) > -1) { + return groups[i]; + } + } + return false; + }; + + /** + * get total column groups + * @returns {*|Array} + */ + var getColGroups = function () { + var result = []; + for (var i = 0, groupsLength = groups.length; i < groupsLength; i++) { + if (Array.isArray(groups[i]['cols'])) { + result.push(groups[i]); + } + } + return result; + }; + + /** + * get total col groups by level + * @param {Number} level + * @returns {*|Array} + */ + var getColGroupsByLevel = function (level) { + var result = []; + for (var i = 0, groupsLength = groups.length; i < groupsLength; i++) { + if (groups[i]['cols'] && groups[i]['level'] === level) { + result.push(groups[i]); + } + } + return result; + }; + + /** + * get total row groups + * @returns {*|Array} + */ + var getRowGroups = function () { + var result = []; + for (var i = 0, groupsLength = groups.length; i < groupsLength; i++) { + if (Array.isArray(groups[i]['rows'])) { + result.push(groups[i]); + } + } + return result; + }; + + /** + * get total row groups by level + * @param {Number} level + * @returns {*|Array} + */ + var getRowGroupsByLevel = function (level) { + var result = []; + for (var i = 0, groupsLength = groups.length; i < groupsLength; i++) { + if (groups[i]['rows'] && groups[i]['level'] === level) { + result.push(groups[i]); + } + } + return result; + }; + + /** + * get last inserted range level in columns + * @param {Array} rangeGroups + * @returns {number} + */ + var getLastLevelColsInRange = function (rangeGroups) { + var level = 0; + + if (rangeGroups.length) { + rangeGroups.forEach(function (items) { + items = items.filter(function (item) { + return item['cols']; + }); + + if (items.length) { + var sortedGroup = items.sort(compare('level', 'desc')), + lastLevel = sortedGroup[0].level; + + if (level < lastLevel) { + level = lastLevel; + } + } + }); + } + + return level; + }; + + /** + * get last inserted range level in rows + * @param {Array} rangeGroups + * @returns {number} + */ + var getLastLevelRowsInRange = function (rangeGroups) { + var level = 0; + + if (rangeGroups.length) { + rangeGroups.forEach(function (items) { + items = items.filter(function (item) { + return item['rows']; + }); + + if (items.length) { + var sortedGroup = items.sort(compare('level', 'desc')), + lastLevel = sortedGroup[0].level; + + if (level < lastLevel) { + level = lastLevel; + } + } + }); + } + + return level; + }; + + /** + * create group for cols + * @param {Number} from + * @param {Number} to + */ + var groupCols = function (from, to) { + var rangeGroups = getRangeGroups("cols", from, to), + lastLevel = getLastLevelColsInRange(rangeGroups.groups); + + if (lastLevel === levels.cols) { + levels.cols++; + } else if (lastLevel > levels.cols) { + levels.cols = lastLevel + 1; + } + + if (!counters.cols) { + counters.cols = getColGroups().length; + } + + counters.cols++; + groups.push({ + id: 'c' + counters.cols, + level: lastLevel + 1, + cols: range(from, to), + hidden: 0 + }); + }; + + /** + * create group for rows + * @param {Number} from + * @param {Number} to + */ + var groupRows = function (from, to) { + var rangeGroups = getRangeGroups("rows", from, to), + lastLevel = getLastLevelRowsInRange(rangeGroups.groups); + + levels.rows = Math.max(levels.rows, lastLevel + 1); + + + if (!counters.rows) { + counters.rows = getRowGroups().length; + } + + counters.rows++; + groups.push({ + id: 'r' + counters.rows, + level: lastLevel + 1, + rows: range(from, to), + hidden: 0 + }); + }; + + /** + * show or hide groups + * @param showHide + * @param groups + */ + var showHideGroups = function (hidden, groups) { + var level; + for (var i = 0, groupsLength = groups.length; i < groupsLength; i++) { + groups[i].hidden = hidden; + level = groups[i].level; + + if (!hiddenRows[level]) { + hiddenRows[level] = []; + } + if (!hiddenCols[level]) { + hiddenCols[level] = []; + } + + if (groups[i].rows) { + for (var j = 0, rowsLength = groups[i].rows.length; j < rowsLength; j++) { + if (hidden > 0) { + hiddenRows[level][groups[i].rows[j]] = true; + } else { + hiddenRows[level][groups[i].rows[j]] = void 0; + } + } + } else if (groups[i].cols) { + for (var j = 0, colsLength = groups[i].cols.length; j < colsLength; j++) { + if (hidden > 0) { + hiddenCols[level][groups[i].cols[j]] = true; + } else { + hiddenCols[level][groups[i].cols[j]] = void 0; + } + } + } + } + }; + + /** + * Check if the next cell of the dimension (row / column) contains a group at the same level + * @param dimension + * @param currentPosition + * @param level + * @param currentGroupId + * @returns {boolean} + */ + var nextIndexSharesLevel = function (dimension, currentPosition, level, currentGroupId) { + var nextCellGroupId + , levelsByOrder; + + switch (dimension) { + case 'rows': + nextCellGroupId = getGroupByRowAndLevel(currentPosition + 1, level).id; + levelsByOrder = Handsontable.Grouping.getGroupLevelsByRows(); + break; + case 'cols': + nextCellGroupId = getGroupByColAndLevel(currentPosition + 1, level).id; + levelsByOrder = Handsontable.Grouping.getGroupLevelsByCols(); + break; + } + + return !!(levelsByOrder[currentPosition + 1] && levelsByOrder[currentPosition + 1].indexOf(level) > -1 && currentGroupId == nextCellGroupId); + + }; + + /** + * Check if the previous cell of the dimension (row / column) contains a group at the same level + * @param dimension + * @param currentPosition + * @param level + * @param currentGroupId + * @returns {boolean} + */ + var previousIndexSharesLevel = function (dimension, currentPosition, level, currentGroupId) { + var previousCellGroupId + , levelsByOrder; + + switch (dimension) { + case 'rows': + previousCellGroupId = getGroupByRowAndLevel(currentPosition - 1, level).id; + levelsByOrder = Handsontable.Grouping.getGroupLevelsByRows(); + break; + case 'cols': + previousCellGroupId = getGroupByColAndLevel(currentPosition - 1, level).id; + levelsByOrder = Handsontable.Grouping.getGroupLevelsByCols(); + break; + } + + return !!(levelsByOrder[currentPosition - 1] && levelsByOrder[currentPosition - 1].indexOf(level) > -1 && currentGroupId == previousCellGroupId); + + }; + + /** + * Check if the provided index is at the end of the group indicator line + * @param dimension + * @param index + * @param level + * @param currentGroupId + * @returns {boolean} + */ + var isLastIndexOfTheLine = function (dimension, index, level, currentGroupId) { + if (index === 0) { + return false; + } + var levelsByOrder + , entriesLength + , previousSharesLevel = previousIndexSharesLevel(dimension, index, level, currentGroupId) + , nextSharesLevel = nextIndexSharesLevel(dimension, index, level, currentGroupId) + , nextIsHidden = false; + + switch (dimension) { + case 'rows': + levelsByOrder = Handsontable.Grouping.getGroupLevelsByRows(); + entriesLength = instance.countRows(); + for (var i = 0; i <= levels.rows; i++) { + if (hiddenRows[i] && hiddenRows[i][index + 1]) { + nextIsHidden = true; + break; + } + } + break; + case 'cols': + levelsByOrder = Handsontable.Grouping.getGroupLevelsByCols(); + entriesLength = instance.countCols(); + for (var i = 0; i <= levels.cols; i++) { + if (hiddenCols[i] && hiddenCols[i][index + 1]) { + nextIsHidden = true; + break; + } + } + break; + } + + if (previousSharesLevel) { + if (index == entriesLength - 1) { + return true; + } else if (!nextSharesLevel || (nextSharesLevel && nextIsHidden)) { + return true; + } else if (!levelsByOrder[index + 1]) { + return true; + } + } + return false; + }; + + /** + * Check if all rows/cols are hidden + * @param dataType + */ + var isLastHidden = function (dataType) { + var levelAmount; + + switch (dataType) { + case 'rows': + levelAmount = levels.rows; + for (var j = 0; j <= levelAmount; j++) { + if (hiddenRows[j] && hiddenRows[j][instance.countRows() - 1]) { + return true; + } + } + + break; + case 'cols': + levelAmount = levels.cols; + for (var j = 0; j <= levelAmount; j++) { + if (hiddenCols[j] && hiddenCols[j][instance.countCols() - 1]) { + return true; + } + } + break; + } + + return false; + }; + + /** + * Check if the provided index is at the beginning of the group indicator line + * @param dimension + * @param index + * @param level + * @param currentGroupId + * @returns {boolean} + */ + var isFirstIndexOfTheLine = function (dimension, index, level, currentGroupId) { + var levelsByOrder + , entriesLength + , currentGroup = getGroupById(currentGroupId) + , previousAreHidden = false + , arePreviousHidden = function (dimension) { + var hidden = false + , hiddenArr = dimension == 'rows' ? hiddenRows : hiddenCols; + for (var i = 0; i <= levels[dimension]; i++) { + tempInd = index; + while (currentGroup[dimension].indexOf(tempInd) > -1) { + hidden = !!(hiddenArr[i] && hiddenArr[i][tempInd]); + tempInd--; + } + if (hidden) { + break; + } + } + return hidden; + } + , previousSharesLevel = previousIndexSharesLevel(dimension, index, level, currentGroupId) + , nextSharesLevel = nextIndexSharesLevel(dimension, index, level, currentGroupId) + , tempInd; + + switch (dimension) { + case 'rows': + levelsByOrder = Handsontable.Grouping.getGroupLevelsByRows(); + entriesLength = instance.countRows(); + previousAreHidden = arePreviousHidden(dimension); + break; + case 'cols': + levelsByOrder = Handsontable.Grouping.getGroupLevelsByCols(); + entriesLength = instance.countCols(); + previousAreHidden = arePreviousHidden(dimension); + break; + } + + if (index == entriesLength - 1) { + return false; + } + else if (index === 0) { + if (nextSharesLevel) { + return true; + } + } else if (!previousSharesLevel || (previousSharesLevel && previousAreHidden)) { + if (nextSharesLevel) { + return true; + } + } else if (!levelsByOrder[index - 1]) { + if (nextSharesLevel) { + return true; + } + } + return false; + }; + + /** + * Add group expander button + * @param dimension + * @param index + * @param level + * @param id + * @param elem + * @returns {*} + */ + var addGroupExpander = function (dataType, index, level, id, elem) { + var previousIndexGroupId; + + switch (dataType) { + case 'rows': + previousIndexGroupId = getGroupByRowAndLevel(index - 1, level).id; + break; + case 'cols': + previousIndexGroupId = getGroupByColAndLevel(index - 1, level).id; + break; + } + + if (!previousIndexGroupId) { + return null; + } + + if (index > 0) { + if (previousIndexSharesLevel(dataType, index - 1, level, previousIndexGroupId) && previousIndexGroupId != id) { + + var expanderButton = document.createElement('DIV'); + Handsontable.Dom.addClass(expanderButton, classes.expandButton); + expanderButton.id = 'htExpand-' + previousIndexGroupId; + expanderButton.appendChild(document.createTextNode('+')); + expanderButton.setAttribute('data-level', level); + expanderButton.setAttribute('data-type', dataType); + expanderButton.setAttribute('data-hidden', "1"); + + elem.appendChild(expanderButton); + + return expanderButton; + } + } + return null; + }; + + /** + * Check if provided cell is collapsed (either by rows or cols) + * @param currentPosition + * @returns {boolean} + */ + var isCollapsed = function (currentPosition) { + var rowGroups = getRowGroups() + , colGroups = getColGroups(); + + for (var i = 0, rowGroupsCount = rowGroups.length; i < rowGroupsCount; i++) { + if (rowGroups[i].rows.indexOf(currentPosition.row) > -1 && rowGroups[i].hidden) { + return true; + } + } + + if (currentPosition.col === null) { // if col is set to null, check only rows + return false; + } + + for (var i = 0, colGroupsCount = colGroups.length; i < colGroupsCount; i++) { + if (colGroups[i].cols.indexOf(currentPosition.col) > -1 && colGroups[i].hidden) { + return true; + } + } + + return false; + }; + + return { + + /** + * all groups for ht instance + */ + getGroups: function () { + return groups; + }, + /** + * All levels for rows and cols respectively + */ + getLevels: function () { + return levels; + }, + /** + * Current instance + */ + instance: instance, + /** + * Initial setting for minSpareRows + */ + baseSpareRows: instance.getSettings().minSpareRows, + /** + * Initial setting for minSpareCols + */ + baseSpareCols: instance.getSettings().minSpareCols, + + getRowGroups: getRowGroups, + getColGroups: getColGroups, + /** + * init group + * @param {Object} settings, could be an array of objects [{cols: [0,1,2]}, {cols: [3,4,5]}, {rows: [0,1]}] + */ + init: function () { + var groupsSetting = instance.getSettings().groups; + if (groupsSetting) { + if (Array.isArray(groupsSetting)) { + Handsontable.Grouping.initGroups(groupsSetting); + } + } + }, + + /** + * init groups from configuration on startup + */ + initGroups: function (initialGroups) { + var that = this; + + groups = []; + + initialGroups.forEach(function (item) { + var _group = [], + isRow = false, + isCol = false; + + if (Array.isArray(item.rows)) { + _group = item.rows; + isRow = true; + } else if (Array.isArray(item.cols)) { + _group = item.cols; + isCol = true; + } + + var from = _group[0], + to = _group[_group.length - 1]; + + if (isRow) { + groupRows(from, to); + } else if (isCol) { + groupCols(from, to); + } + }); +// this.render(); + }, + + /** + * Remove all existing groups + */ + resetGroups: function () { + groups = []; + counters = { + rows: 0, + cols: 0 + }; + levels = { + rows: 0, + cols: 0 + }; + + var allOccurrences; + for (var i in classes) { + if (typeof classes[i] != 'function') { + allOccurrences = document.querySelectorAll('.' + classes[i]); + for (var j = 0, occurrencesLength = allOccurrences.length; j < occurrencesLength; j++) { + Handsontable.Dom.removeClass(allOccurrences[j], classes[i]); + } + } + } + + var otherClasses = ['htGroupColClosest', 'htGroupCol']; + for (var i = 0, otherClassesLength = otherClasses.length; i < otherClassesLength; i++) { + allOccurrences = document.querySelectorAll('.' + otherClasses[i]); + for (var j = 0, occurrencesLength = allOccurrences.length; j < occurrencesLength; j++) { + Handsontable.Dom.removeClass(allOccurrences[j], otherClasses[i]); + } + } + }, + /** + * Update groups from the instance settings + */ + updateGroups: function () { + var groupSettings = this.getSettings().groups; + + Handsontable.Grouping.resetGroups(); + Handsontable.Grouping.initGroups(groupSettings); + }, + afterGetRowHeader: function (row, TH) { + var currentRowHidden = false; + for (var i = 0, levels = hiddenRows.length; i < levels; i++) { + if (hiddenRows[i] && hiddenRows[i][row] === true) { + currentRowHidden = true; + } + } + + if (currentRowHidden) { + Handsontable.Dom.addClass(TH.parentNode, 'hidden'); + } else if (!currentRowHidden && Handsontable.Dom.hasClass(TH.parentNode, 'hidden')) { + Handsontable.Dom.removeClass(TH.parentNode, 'hidden'); + } + + }, + afterGetColHeader: function (col, TH) { + var rowHeaders = this.view.wt.wtSettings.getSetting('rowHeaders').length + , thisColgroup = instance.rootElement.querySelectorAll('colgroup col:nth-child(' + parseInt(col + rowHeaders + 1, 10) + ')'); + + if (thisColgroup.length === 0) { + return; + } + + var currentColHidden = false; + for (var i = 0, levels = hiddenCols.length; i < levels; i++) { + if (hiddenCols[i] && hiddenCols[i][col] === true) { + currentColHidden = true; + } + } + + if (currentColHidden) { + for (var i = 0, colsAmount = thisColgroup.length; i < colsAmount; i++) { + Handsontable.Dom.addClass(thisColgroup[i], 'hidden'); + } + } else if (!currentColHidden && Handsontable.Dom.hasClass(thisColgroup[0], 'hidden')) { + for (var i = 0, colsAmount = thisColgroup.length; i < colsAmount; i++) { + Handsontable.Dom.removeClass(thisColgroup[i], 'hidden'); + } + } + }, + /** + * Create a renderer for additional row/col headers, acting as group indicators + * @param walkontableConfig + * @param direction + */ + groupIndicatorsFactory: function (renderersArr, direction) { + var groupsLevelsList + , getCurrentLevel + , getCurrentGroupId + , dataType + , getGroupByIndexAndLevel + , headersType + , currentHeaderModifier + , createLevelTriggers; + + switch (direction) { + case 'horizontal': + groupsLevelsList = Handsontable.Grouping.getGroupLevelsByCols(); + getCurrentLevel = function (elem) { + return Array.prototype.indexOf.call(elem.parentNode.parentNode.childNodes, elem.parentNode) + 1; + }; + getCurrentGroupId = function (col, level) { + return getGroupByColAndLevel(col, level).id; + }; + dataType = 'cols'; + getGroupByIndexAndLevel = function (col, level) { + return getGroupByColAndLevel(col - 1, level); + }; + headersType = "columnHeaders"; + currentHeaderModifier = function (headerRenderers) { + if (headerRenderers.length === 1) { + var oldFn = headerRenderers[0]; + + headerRenderers[0] = function (index, elem, level) { + + if (index < -1) { + makeGroupIndicatorsForLevel()(index, elem, level); + } else { + Handsontable.Dom.removeClass(elem, classes.groupIndicatorContainer); + oldFn(index, elem, level); + } + }; + } + return function () { + return headerRenderers; + }; + }; + createLevelTriggers = true; + break; + case 'vertical': + groupsLevelsList = Handsontable.Grouping.getGroupLevelsByRows(); + getCurrentLevel = function (elem) { + return Handsontable.Dom.index(elem) + 1; + }; + getCurrentGroupId = function (row, level) { + return getGroupByRowAndLevel(row, level).id; + }; + dataType = 'rows'; + getGroupByIndexAndLevel = function (row, level) { + return getGroupByRowAndLevel(row - 1, level); + }; + headersType = "rowHeaders"; + currentHeaderModifier = function (headerRenderers) { + return headerRenderers; + }; + break; + } + + var createButton = function (parent) { + var button = document.createElement('div'); + + parent.appendChild(button); + + return { + button: button, + addClass: function (className) { + Handsontable.Dom.addClass(button, className); + } + }; + }; + + var makeGroupIndicatorsForLevel = function () { + var directionClassname = direction.charAt(0).toUpperCase() + direction.slice(1); // capitalize the first letter + + return function (index, elem, level) { // header rendering function + + level++; + var child + , collapseButton; + + /* jshint -W084 */ + while (child = elem.lastChild) { + elem.removeChild(child); + } + + Handsontable.Dom.addClass(elem, classes.groupIndicatorContainer); + + var currentGroupId = getCurrentGroupId(index, level); + + if (index > -1 && (groupsLevelsList[index] && groupsLevelsList[index].indexOf(level) > -1)) { + + collapseButton = createButton(elem); + collapseButton.addClass(classes.groupIndicator(directionClassname)); + + if (isFirstIndexOfTheLine(dataType, index, level, currentGroupId)) { // add a little thingy and the top of the group indicator + collapseButton.addClass(classes.groupStart); + } + + if (isLastIndexOfTheLine(dataType, index, level, currentGroupId)) { // add [+]/[-] button at the end of the line + collapseButton.button.appendChild(document.createTextNode('-')); + collapseButton.addClass(classes.collapseButton); + collapseButton.button.id = classes.collapseGroupId(currentGroupId); + collapseButton.button.setAttribute('data-level', level); + collapseButton.button.setAttribute('data-type', dataType); + } + + } + + if (createLevelTriggers) { + var rowInd = Handsontable.Dom.index(elem.parentNode); + if (index === -1 || (index < -1 && rowInd === Handsontable.Grouping.getLevels().cols + 1) || + (rowInd === 0 && Handsontable.Grouping.getLevels().cols === 0)) { + collapseButton = createButton(elem); + collapseButton.addClass(classes.levelTrigger); + + if (index === -1) { + collapseButton.button.id = classes.collapseFromLevel("Cols", level); + collapseButton.button.appendChild(document.createTextNode(level)); + } else if (index < -1 && rowInd === Handsontable.Grouping.getLevels().cols + 1 || + (rowInd === 0 && Handsontable.Grouping.getLevels().cols === 0)) { + var colInd = Handsontable.Dom.index(elem) + 1; + collapseButton.button.id = classes.collapseFromLevel("Rows", colInd); + collapseButton.button.appendChild(document.createTextNode(colInd)); + } + } + } + + // add group expending button + var expanderButton = addGroupExpander(dataType, index, level, currentGroupId, elem); + if (index > 0) { + var previousGroupObj = getGroupByIndexAndLevel(index - 1, level); + + if (expanderButton && previousGroupObj.hidden) { + Handsontable.Dom.addClass(expanderButton, classes.clickable); + } + } + + updateHeaderWidths(); + + }; + }; + + + renderersArr = currentHeaderModifier(renderersArr); + + + if (counters[dataType] > 0) { + for (var i = 0; i < levels[dataType] + 1; i++) { // for each level of col groups add a header renderer + if (!Array.isArray(renderersArr)) { + renderersArr = typeof renderersArr === 'function' ? renderersArr() : new Array(renderersArr); + } + renderersArr.unshift(makeGroupIndicatorsForLevel()); + } + } + }, + /** + * Get group levels array arranged by rows + * @returns {Array} + */ + getGroupLevelsByRows: function () { + var rowGroups = getRowGroups() + , result = []; + + for (var i = 0, groupsLength = rowGroups.length; i < groupsLength; i++) { + if (rowGroups[i].rows) { + for (var j = 0, groupRowsLength = rowGroups[i].rows.length; j < groupRowsLength; j++) { + if (!result[rowGroups[i].rows[j]]) { + result[rowGroups[i].rows[j]] = []; + } + result[rowGroups[i].rows[j]].push(rowGroups[i].level); + } + } + } + return result; + }, + /** + * Get group levels array arranged by cols + * @returns {Array} + */ + getGroupLevelsByCols: function () { + var colGroups = getColGroups() + , result = []; + + for (var i = 0, groupsLength = colGroups.length; i < groupsLength; i++) { + if (colGroups[i].cols) { + for (var j = 0, groupColsLength = colGroups[i].cols.length; j < groupColsLength; j++) { + if (!result[colGroups[i].cols[j]]) { + result[colGroups[i].cols[j]] = []; + } + result[colGroups[i].cols[j]].push(colGroups[i].level); + } + } + } + return result; + }, + /** + * Toggle the group visibility ( + / - event handler) + * @param event + * @param coords + * @param TD + */ + toggleGroupVisibility: function (event, coords, TD) { + if (Handsontable.Dom.hasClass(event.target, classes.expandButton) || + Handsontable.Dom.hasClass(event.target, classes.collapseButton) || + Handsontable.Dom.hasClass(event.target, classes.levelTrigger)) { + var element = event.target + , elemIdSplit = element.id.split('-'); + + var groups = [] + , id + , level + , type + , hidden; + + var prepareGroupData = function (componentElement) { + if (componentElement) { + element = componentElement; + } + + elemIdSplit = element.id.split('-'); + + id = elemIdSplit[1]; + level = parseInt(element.getAttribute('data-level'), 10); + type = element.getAttribute('data-type'); + hidden = parseInt(element.getAttribute('data-hidden')); + + if (isNaN(hidden)) { + hidden = 1; + } else { + hidden = (hidden ? 0 : 1); + } + + element.setAttribute('data-hidden', hidden.toString()); + + + groups.push(getGroupById(id)); + }; + + if (element.className.indexOf(classes.levelTrigger) > -1) { // show levels below, hide all above + var groupsInLevel + , groupsToExpand = [] + , groupsToCollapse = [] + , levelType = element.id.indexOf("Rows") > -1 ? "rows" : "cols"; + + for (var i = 1, levelsCount = levels[levelType]; i <= levelsCount; i++) { + groupsInLevel = levelType == "rows" ? getRowGroupsByLevel(i) : getColGroupsByLevel(i); + + if (i >= parseInt(elemIdSplit[1], 10)) { + for (var j = 0, groupCount = groupsInLevel.length; j < groupCount; j++) { + groupsToCollapse.push(groupsInLevel[j]); + } + } else { + for (var j = 0, groupCount = groupsInLevel.length; j < groupCount; j++) { + groupsToExpand.push(groupsInLevel[j]); + } + } + } + + showHideGroups(true, groupsToCollapse); + showHideGroups(false, groupsToExpand); + + } else { + prepareGroupData(); + showHideGroups(hidden, groups); + } + + // add the expander button to a dummy spare row/col, if no longer needed -> remove it + /* jshint -W038 */ + type = type || levelType; + + var lastHidden = isLastHidden(type) + , typeUppercase = type.charAt(0).toUpperCase() + type.slice(1) + , spareElements = Handsontable.Grouping['baseSpare' + typeUppercase]; + + if (lastHidden) { + /* jshint -W041 */ + if (spareElements == 0) { + instance.alter('insert_' + type.slice(0, -1), instance['count' + typeUppercase]()); + Handsontable.Grouping["dummy" + type.slice(0, -1)] = true; + } + } else { + /* jshint -W041 */ + if (spareElements == 0) { + if (Handsontable.Grouping["dummy" + type.slice(0, -1)]) { + instance.alter('remove_' + type.slice(0, -1), instance['count' + typeUppercase]() - 1); + Handsontable.Grouping["dummy" + type.slice(0, -1)] = false; + } + } + } + + instance.render(); + + event.stopImmediatePropagation(); + } + }, + /** + * Modify the delta when changing cells using keyobard + * @param position + * @returns {Function} + */ + modifySelectionFactory: function (position) { + var instance = this.instance; + var currentlySelected + , nextPosition = new WalkontableCellCoords(0, 0) + , nextVisible = function (direction, currentPosition) { // updates delta to skip to the next visible cell + var updateDelta = 0; + + switch (direction) { + case 'down': + while (isCollapsed(currentPosition)) { + updateDelta++; + currentPosition.row += 1; + } + break; + case 'up': + while (isCollapsed(currentPosition)) { + updateDelta--; + currentPosition.row -= 1; + } + break; + case 'right': + while (isCollapsed(currentPosition)) { + updateDelta++; + currentPosition.col += 1; + } + break; + case 'left': + while (isCollapsed(currentPosition)) { + updateDelta--; + currentPosition.col -= 1; + } + break; + } + + return updateDelta; + } + , updateDelta = function (delta, nextPosition) { + if (delta.row > 0) { // moving down + if (isCollapsed(nextPosition)) { + delta.row += nextVisible('down', nextPosition); + } + } else if (delta.row < 0) { // moving up + if (isCollapsed(nextPosition)) { + delta.row += nextVisible('up', nextPosition); + } + } + + if (delta.col > 0) { // moving right + if (isCollapsed(nextPosition)) { + delta.col += nextVisible('right', nextPosition); + } + } else if (delta.col < 0) { // moving left + if (isCollapsed(nextPosition)) { + delta.col += nextVisible('left', nextPosition); + } + } + }; + + /* jshint -W027 */ + switch (position) { + case 'start': + return function (delta) { + currentlySelected = instance.getSelected(); + nextPosition.row = currentlySelected[0] + delta.row; + nextPosition.col = currentlySelected[1] + delta.col; + + updateDelta(delta, nextPosition); + }; + break; + case 'end': + return function (delta) { + currentlySelected = instance.getSelected(); + nextPosition.row = currentlySelected[2] + delta.row; + nextPosition.col = currentlySelected[3] + delta.col; + + updateDelta(delta, nextPosition); + }; + break; + } + }, + modifyRowHeight: function (height, row) { + if (instance.view.wt.wtTable.rowFilter && isCollapsed({row: row, col: null})) { + return 0; + } + }, + validateGroups: function () { + + var areRangesOverlapping = function (a, b) { + if ((a[0] < b[0] && a[1] < b[1] && b[0] <= a[1]) || + (a[0] > b[0] && b[1] < a[1] && a[0] <= b[1])) { + return true; + } + }; + + var configGroups = instance.getSettings().groups + , cols = [] + , rows = []; + + for (var i = 0, groupsLength = configGroups.length; i < groupsLength; i++) { + if (configGroups[i].rows) { + /* jshint -W027 */ + if(configGroups[i].rows.length === 1) { // single-entry group + throw new Error("Grouping error: Group {" + configGroups[i].rows[0] + "} is invalid. Cannot define single-entry groups."); + return false; + } else if(configGroups[i].rows.length === 0) { + throw new Error("Grouping error: Cannot define empty groups."); + return false; + } + + rows.push(configGroups[i].rows); + + for (var j = 0, rowsLength = rows.length; j < rowsLength; j++) { + if (areRangesOverlapping(configGroups[i].rows, rows[j])) { + + throw new Error("Grouping error: ranges {" + configGroups[i].rows[0] + ", " + configGroups[i].rows[1] + "} and {" + rows[j][0] + ", " + rows[j][1] + "} are overlapping."); + return false; + } + } + } else if (configGroups[i].cols) { + + if(configGroups[i].cols.length === 1) { // single-entry group + throw new Error("Grouping error: Group {" + configGroups[i].cols[0] + "} is invalid. Cannot define single-entry groups."); + return false; + } else if(configGroups[i].cols.length === 0) { + throw new Error("Grouping error: Cannot define empty groups."); + return false; + } + + cols.push(configGroups[i].cols); + + for (var j = 0, colsLength = cols.length; j < colsLength; j++) { + if (areRangesOverlapping(configGroups[i].cols, cols[j])) { + + throw new Error("Grouping error: ranges {" + configGroups[i].cols[0] + ", " + configGroups[i].cols[1] + "} and {" + cols[j][0] + ", " + cols[j][1] + "} are overlapping."); + return false; + } + } + } + } + + return true; + }, + afterGetRowHeaderRenderers: function (arr) { + Handsontable.Grouping.groupIndicatorsFactory(arr, 'vertical'); + }, + afterGetColumnHeaderRenderers: function (arr) { + Handsontable.Grouping.groupIndicatorsFactory(arr, 'horizontal'); + }, + hookProxy: function (fn, arg) { + return function () { + if (instance.getSettings().groups) { + return arg ? Handsontable.Grouping[fn](arg).apply(this, arguments) : Handsontable.Grouping[fn].apply(this, arguments); + } else { + return void 0; + } + }; + } + }; +}; + +/** + * create new instance + */ +var init = function () { + var instance = this, + groupingSetting = !!(instance.getSettings().groups); + + + if (groupingSetting) { + var headerUpdates = {}; + + Handsontable.Grouping = new Grouping(instance); + + if (!instance.getSettings().rowHeaders) { // force using rowHeaders -- needs to be changed later + headerUpdates.rowHeaders = true; + } + if (!instance.getSettings().colHeaders) { // force using colHeaders -- needs to be changed later + headerUpdates.colHeaders = true; + } + if (headerUpdates.colHeaders || headerUpdates.rowHeaders) { + instance.updateSettings(headerUpdates); + } + + var groupConfigValid = Handsontable.Grouping.validateGroups(); + if (!groupConfigValid) { + return; + } + + instance.addHook('beforeInit', Handsontable.Grouping.hookProxy('init')); + instance.addHook('afterUpdateSettings', Handsontable.Grouping.hookProxy('updateGroups')); + instance.addHook('afterGetColumnHeaderRenderers', Handsontable.Grouping.hookProxy('afterGetColumnHeaderRenderers')); + instance.addHook('afterGetRowHeaderRenderers', Handsontable.Grouping.hookProxy('afterGetRowHeaderRenderers')); + instance.addHook('afterGetRowHeader', Handsontable.Grouping.hookProxy('afterGetRowHeader')); + instance.addHook('afterGetColHeader', Handsontable.Grouping.hookProxy('afterGetColHeader')); + instance.addHook('beforeOnCellMouseDown', Handsontable.Grouping.hookProxy('toggleGroupVisibility')); + instance.addHook('modifyTransformStart', Handsontable.Grouping.hookProxy('modifySelectionFactory', 'start')); + instance.addHook('modifyTransformEnd', Handsontable.Grouping.hookProxy('modifySelectionFactory', 'end')); + instance.addHook('modifyRowHeight', Handsontable.Grouping.hookProxy('modifyRowHeight')); + } +}; + +/** + * Update headers widths for the group indicators + */ +// TODO: this needs cleaning up +var updateHeaderWidths = function () { + var colgroups = document.querySelectorAll('colgroup'); + for (var i = 0, colgroupsLength = colgroups.length; i < colgroupsLength; i++) { + var rowHeaders = colgroups[i].querySelectorAll('col.rowHeader'); + if (rowHeaders.length === 0) { + return; + } + for (var j = 0, rowHeadersLength = rowHeaders.length + 1; j < rowHeadersLength; j++) { + if (rowHeadersLength == 2) { + return; + } + if (j < Handsontable.Grouping.getLevels().rows + 1) { + if (j == Handsontable.Grouping.getLevels().rows) { + Handsontable.Dom.addClass(rowHeaders[j], 'htGroupColClosest'); + } else { + Handsontable.Dom.addClass(rowHeaders[j], 'htGroupCol'); + } + } + } + } +}; + +Handsontable.hooks.add('beforeInit', init); + +Handsontable.hooks.add('afterUpdateSettings', function () { + + if (this.getSettings().groups && !Handsontable.Grouping) { + init.call(this, arguments); + } else if (!this.getSettings().groups && Handsontable.Grouping) { + Handsontable.Grouping.resetGroups(); + Handsontable.Grouping = void 0; + } +}); + +Handsontable.plugins.Grouping = Grouping; + +(function (Handsontable) { + /** + * Plugin used to allow user to copy and paste from the context menu + * Currently uses ZeroClipboard due to browser limitations + * @constructor + */ + function ContextMenuCopyPaste() { + this.zeroClipboardInstance = null; + this.instance = null; + } + + /** + * Configure ZeroClipboard + */ + ContextMenuCopyPaste.prototype.prepareZeroClipboard = function () { + if(this.swfPath) { + ZeroClipboard.config({ + swfPath: this.swfPath + }); + } + }; + + /** + * Copy action + * @returns {CopyPasteClass.elTextarea.value|*} + */ + ContextMenuCopyPaste.prototype.copy = function () { + this.instance.copyPaste.setCopyableText(); + return this.instance.copyPaste.copyPasteInstance.elTextarea.value; + }; + + /** + * Adds copy/paste items to context menu + */ + ContextMenuCopyPaste.prototype.addToContextMenu = function (defaultOptions) { + if (!this.getSettings().contextMenuCopyPaste) { + return; + } + + defaultOptions.items.unshift( + { + key: 'copy', + name: 'Copy' + }, + { + key: 'paste', + name: 'Paste', + callback: function () { + this.copyPaste.triggerPaste(); + } + }, + Handsontable.ContextMenu.SEPARATOR + ); + }; + + /** + * Setup ZeroClipboard swf clip position and event handlers + * @param cmInstance Current context menu instance + */ + ContextMenuCopyPaste.prototype.setupZeroClipboard = function (cmInstance) { + var plugin = this; + this.cmInstance = cmInstance; + + if (!Handsontable.Dom.hasClass(this.cmInstance.rootElement, 'htContextMenu')) { + return; + } + + var data = cmInstance.getData(); + for (var i = 0, ilen = data.length; i < ilen; i++) { //find position of 'copy' option + /*jshint -W083 */ + if (data[i].key === 'copy') { + this.zeroClipboardInstance = new ZeroClipboard(cmInstance.getCell(i, 0)); + + this.zeroClipboardInstance.off(); + this.zeroClipboardInstance.on("copy", function (event) { + var clipboard = event.clipboardData; + clipboard.setData("text/plain", plugin.copy()); + plugin.instance.getSettings().outsideClickDeselects = plugin.outsideClickDeselectsCache; + }); + + cmCopyPaste.bindEvents(); + break; + } + } + }; + + /** + * Bind all the standard events + */ + ContextMenuCopyPaste.prototype.bindEvents = function () { + var plugin = this; + + // Workaround for 'current' and 'zeroclipboard-is-hover' classes being stuck when moving the cursor over the context menu + if (plugin.cmInstance) { + + var eventManager = new Handsontable.eventManager(this.instance); + + var removeCurrenClass = function (event) { + var hadClass = plugin.cmInstance.rootElement.querySelector('td.current'); + if (hadClass) { + Handsontable.Dom.removeClass(hadClass, 'current'); + } + plugin.outsideClickDeselectsCache = plugin.instance.getSettings().outsideClickDeselects; + plugin.instance.getSettings().outsideClickDeselects = false; + }; + + var removeZeroClipboardClass = function (event) { + var hadClass = plugin.cmInstance.rootElement.querySelector('td.zeroclipboard-is-hover'); + if (hadClass) { + Handsontable.Dom.removeClass(hadClass, 'zeroclipboard-is-hover'); + } + plugin.instance.getSettings().outsideClickDeselects = plugin.outsideClickDeselectsCache; + }; + + eventManager.removeEventListener(document,'mouseenter', function () { + removeCurrenClass(); + }); + eventManager.addEventListener(document, 'mouseenter', function (e) { + removeCurrenClass(); + }); + + eventManager.removeEventListener(document,'mouseleave', function () { + removeZeroClipboardClass(); + }); + eventManager.addEventListener(document, 'mouseleave', function (e) { + removeZeroClipboardClass(); + }); + + + } + }; + + /** + * Initialize plugin + * @returns {boolean} Returns false if ZeroClipboard is not properly included + */ + ContextMenuCopyPaste.prototype.init = function () { + if (!this.getSettings().contextMenuCopyPaste) { + return; + } else if (typeof this.getSettings().contextMenuCopyPaste == "object") { + cmCopyPaste.swfPath = this.getSettings().contextMenuCopyPaste.swfPath; + } + + /* jshint ignore:start */ + if (typeof ZeroClipboard === 'undefined') { + throw new Error("To be able to use the Copy/Paste feature from the context menu, you need to manualy include ZeroClipboard.js file to your website."); + + return false; + } + try { + var flashTest = new ActiveXObject('ShockwaveFlash.ShockwaveFlash'); + } catch(exception) { + if (!('undefined' != typeof navigator.mimeTypes['application/x-shockwave-flash'])) { + throw new Error("To be able to use the Copy/Paste feature from the context menu, your browser needs to have Flash Plugin installed."); + + return false; + } + } + /* jshint ignore:end */ + + cmCopyPaste.instance = this; + cmCopyPaste.prepareZeroClipboard(); + }; + + var cmCopyPaste = new ContextMenuCopyPaste(); + + Handsontable.hooks.add('afterRender', function () { + cmCopyPaste.setupZeroClipboard(this); + }); + + Handsontable.hooks.add('afterInit', cmCopyPaste.init); + Handsontable.hooks.add('afterContextMenuDefaultOptions', cmCopyPaste.addToContextMenu); + Handsontable.ContextMenuCopyPaste = ContextMenuCopyPaste; + +})(Handsontable); + +(function (Handsontable) { + 'use strict'; + + function MultipleSelectionHandles(instance) { + this.instance = instance; + this.dragged = []; + + this.eventManager = Handsontable.eventManager(instance); + + this.bindTouchEvents(); + } + + MultipleSelectionHandles.prototype.getCurrentRangeCoords = function (selectedRange, currentTouch, touchStartDirection, currentDirection, draggedHandle) { + var topLeftCorner = selectedRange.getTopLeftCorner() + , bottomRightCorner = selectedRange.getBottomRightCorner() + , bottomLeftCorner = selectedRange.getBottomLeftCorner() + , topRightCorner = selectedRange.getTopRightCorner(); + + var newCoords = { + start: null, + end: null + }; + + switch (touchStartDirection) { + case "NE-SW": + switch (currentDirection) { + case "NE-SW": + case "NW-SE": + if (draggedHandle == "topLeft") { + newCoords = { + start: new WalkontableCellCoords(currentTouch.row, selectedRange.highlight.col), + end: new WalkontableCellCoords(bottomLeftCorner.row, currentTouch.col) + }; + } else { + newCoords = { + start: new WalkontableCellCoords(selectedRange.highlight.row, currentTouch.col), + end: new WalkontableCellCoords(currentTouch.row, topLeftCorner.col) + }; + } + break; + case "SE-NW": + if (draggedHandle == "bottomRight") { + newCoords = { + start: new WalkontableCellCoords(bottomRightCorner.row, currentTouch.col), + end: new WalkontableCellCoords(currentTouch.row, topLeftCorner.col) + }; + } + break; + //case "SW-NE": + // break; + } + break; + case "NW-SE": + switch (currentDirection) { + case "NE-SW": + if (draggedHandle == "topLeft") { + newCoords = { + start: currentTouch, + end: bottomLeftCorner + }; + } else { + newCoords.end = currentTouch; + } + break; + case "NW-SE": + if (draggedHandle == "topLeft") { + newCoords = { + start: currentTouch, + end: bottomRightCorner + }; + } else { + newCoords.end = currentTouch; + } + break; + case "SE-NW": + if (draggedHandle == "topLeft") { + newCoords = { + start: currentTouch, + end: topLeftCorner + }; + } else { + newCoords.end = currentTouch; + } + break; + case "SW-NE": + if (draggedHandle == "topLeft") { + newCoords = { + start: currentTouch, + end: topRightCorner + }; + } else { + newCoords.end = currentTouch; + } + break; + } + break; + case "SW-NE": + switch (currentDirection) { + case "NW-SE": + if (draggedHandle == "bottomRight") { + newCoords = { + start: new WalkontableCellCoords(currentTouch.row, topLeftCorner.col), + end: new WalkontableCellCoords(bottomLeftCorner.row, currentTouch.col) + }; + } else { + newCoords = { + start: new WalkontableCellCoords(topLeftCorner.row, currentTouch.col), + end: new WalkontableCellCoords(currentTouch.row, bottomRightCorner.col) + }; + } + break; + //case "NE-SW": + // + // break; + case "SW-NE": + if (draggedHandle == "topLeft") { + newCoords = { + start: new WalkontableCellCoords(selectedRange.highlight.row, currentTouch.col), + end: new WalkontableCellCoords(currentTouch.row, bottomRightCorner.col) + }; + } else { + newCoords = { + start: new WalkontableCellCoords(currentTouch.row, topLeftCorner.col), + end: new WalkontableCellCoords(topLeftCorner.row, currentTouch.col) + }; + } + break; + case "SE-NW": + if (draggedHandle == "bottomRight") { + newCoords = { + start: new WalkontableCellCoords(currentTouch.row, topRightCorner.col), + end: new WalkontableCellCoords(topLeftCorner.row, currentTouch.col) + }; + } else if (draggedHandle == "topLeft") { + newCoords = { + start: bottomLeftCorner, + end: currentTouch + }; + } + break; + } + break; + case "SE-NW": + switch (currentDirection) { + case "NW-SE": + case "NE-SW": + case "SW-NE": + if (draggedHandle == "topLeft") { + newCoords.end = currentTouch; + } + break; + case "SE-NW": + if (draggedHandle == "topLeft") { + newCoords.end = currentTouch; + } else { + newCoords = { + start: currentTouch, + end: topLeftCorner + }; + } + break; + } + break; + } + + return newCoords; + }; + + MultipleSelectionHandles.prototype.bindTouchEvents = function () { + var that = this; + var removeFromDragged = function (query) { + + if (this.dragged.length == 1) { + this.dragged = []; + return true; + } + + var entryPosition = this.dragged.indexOf(query); + + if (entryPosition == -1) { + return false; + } else if (entryPosition === 0) { + this.dragged = this.dragged.slice(0, 1); + } else if (entryPosition == 1) { + this.dragged = this.dragged.slice(-1); + } + }; + + this.eventManager.addEventListener(this.instance.rootElement,'touchstart', function (event) { + if(Handsontable.Dom.hasClass(event.target, "topLeftSelectionHandle-HitArea")) { + that.dragged.push("topLeft"); + var selectedRange = that.instance.getSelectedRange(); + that.touchStartRange = { + width: selectedRange.getWidth(), + height: selectedRange.getHeight(), + direction: selectedRange.getDirection() + }; + event.preventDefault(); + + return false; + } else if (Handsontable.Dom.hasClass(event.target, "bottomRightSelectionHandle-HitArea")) { + that.dragged.push("bottomRight"); + var selectedRange = that.instance.getSelectedRange(); + that.touchStartRange = { + width: selectedRange.getWidth(), + height: selectedRange.getHeight(), + direction: selectedRange.getDirection() + }; + event.preventDefault(); + + return false; + } + }); + + this.eventManager.addEventListener(this.instance.rootElement,'touchend', function (event) { + if(Handsontable.Dom.hasClass(event.target, "topLeftSelectionHandle-HitArea")) { + removeFromDragged.call(that, "topLeft"); + that.touchStartRange = void 0; + event.preventDefault(); + + return false; + } else if (Handsontable.Dom.hasClass(event.target, "bottomRightSelectionHandle-HitArea")) { + removeFromDragged.call(that, "bottomRight"); + that.touchStartRange = void 0; + event.preventDefault(); + + return false; + } + }); + + this.eventManager.addEventListener(this.instance.rootElement,'touchmove', function (event) { + var scrollTop = Handsontable.Dom.getWindowScrollTop() + , scrollLeft = Handsontable.Dom.getWindowScrollLeft(); + + if (that.dragged.length > 0) { + var endTarget = document.elementFromPoint( + event.touches[0].screenX - scrollLeft, + event.touches[0].screenY - scrollTop + ); + + if(!endTarget) { + return; + } + + if (endTarget.nodeName == "TD" || endTarget.nodeName == "TH") { + var targetCoords = that.instance.getCoords(endTarget); + + if(targetCoords.col == -1) { + targetCoords.col = 0; + } + + var selectedRange = that.instance.getSelectedRange() + , rangeWidth = selectedRange.getWidth() + , rangeHeight = selectedRange.getHeight() + , rangeDirection = selectedRange.getDirection(); + + if (rangeWidth == 1 && rangeHeight == 1) { + that.instance.selection.setRangeEnd(targetCoords); + } + + var newRangeCoords = that.getCurrentRangeCoords(selectedRange, targetCoords, that.touchStartRange.direction, rangeDirection, that.dragged[0]); + + if(newRangeCoords.start != null) { + that.instance.selection.setRangeStart(newRangeCoords.start); + } + that.instance.selection.setRangeEnd(newRangeCoords.end); + + } + + event.preventDefault(); + } + }); + + }; + + MultipleSelectionHandles.prototype.isDragged = function () { + if (this.dragged.length === 0) { + return false; + } else { + return true; + } + }; + + var init = function () { + var instance = this; + + Handsontable.plugins.multipleSelectionHandles = new MultipleSelectionHandles(instance); + }; + + Handsontable.hooks.add('afterInit', init); + +})(Handsontable); + +var TouchScroll = (function(instance) { + + function TouchScroll(instance) {} + + TouchScroll.prototype.init = function(instance) { + this.instance = instance; + this.bindEvents(); + + this.scrollbars = [ + this.instance.view.wt.wtScrollbars.vertical, + this.instance.view.wt.wtScrollbars.horizontal, + this.instance.view.wt.wtScrollbars.corner + ]; + + this.clones = [ + this.instance.view.wt.wtScrollbars.vertical.clone.wtTable.holder.parentNode, + this.instance.view.wt.wtScrollbars.horizontal.clone.wtTable.holder.parentNode, + this.instance.view.wt.wtScrollbars.corner.clone.wtTable.holder.parentNode + ]; + }; + + TouchScroll.prototype.bindEvents = function () { + var that = this; + + this.instance.addHook('beforeTouchScroll', function () { + Handsontable.freezeOverlays = true; + + for(var i = 0, cloneCount = that.clones.length; i < cloneCount ; i++) { + Handsontable.Dom.addClass(that.clones[i], 'hide-tween'); + } + }); + + this.instance.addHook('afterMomentumScroll', function () { + Handsontable.freezeOverlays = false; + + for(var i = 0, cloneCount = that.clones.length; i < cloneCount ; i++) { + Handsontable.Dom.removeClass(that.clones[i], 'hide-tween'); + } + + for(var i = 0, cloneCount = that.clones.length; i < cloneCount ; i++) { + Handsontable.Dom.addClass(that.clones[i], 'show-tween'); + } + + setTimeout(function () { + for(var i = 0, cloneCount = that.clones.length; i < cloneCount ; i++) { + Handsontable.Dom.removeClass(that.clones[i], 'show-tween'); + } + },400); + + for(var i = 0, cloneCount = that.scrollbars.length; i < cloneCount ; i++) { + that.scrollbars[i].refresh(); + that.scrollbars[i].resetFixedPosition(); + } + + }); + + }; + + return TouchScroll; +}()); + +var touchScrollHandler = new TouchScroll(); + +Handsontable.hooks.add('afterInit', function() { + touchScrollHandler.init.call(touchScrollHandler, this); +}); + +(function (Handsontable) { + function ManualColumnFreeze(instance) { + var fixedColumnsCount = instance.getSettings().fixedColumnsLeft; + + var init = function () { + // update plugin usages count for manualColumnPositions + if (typeof instance.manualColumnPositionsPluginUsages != 'undefined') { + instance.manualColumnPositionsPluginUsages.push('manualColumnFreeze'); + } else { + instance.manualColumnPositionsPluginUsages = ['manualColumnFreeze']; + } + + bindHooks(); + }; + + /** + * Modifies the default Context Menu entry list to consist 'freeze/unfreeze this column' entries + * @param {Object} defaultOptions + */ + function addContextMenuEntry(defaultOptions) { + defaultOptions.items.push( + Handsontable.ContextMenu.SEPARATOR, + { + key: 'freeze_column', + name: function () { + var selectedColumn = instance.getSelected()[1]; + if (selectedColumn > fixedColumnsCount - 1) { + return 'Freeze this column'; + } else { + return 'Unfreeze this column'; + } + }, + disabled: function () { + var selection = instance.getSelected(); + return selection[1] !== selection[3]; + }, + callback: function () { + var selectedColumn = instance.getSelected()[1]; + if (selectedColumn > fixedColumnsCount - 1) { + freezeColumn(selectedColumn); + } else { + unfreezeColumn(selectedColumn); + } + } + } + ); + } + + /** + * Increments the fixed columns count by one + */ + function addFixedColumn() { + instance.updateSettings({ + fixedColumnsLeft: fixedColumnsCount + 1 + }); + fixedColumnsCount++; + } + + /** + * Decrements the fixed columns count by one + */ + function removeFixedColumn() { + instance.updateSettings({ + fixedColumnsLeft: fixedColumnsCount - 1 + }); + fixedColumnsCount--; + } + + /** + * Checks whether 'manualColumnPositions' array needs creating and/or initializing + * @param {Number} [col] + */ + function checkPositionData(col) { + if (!instance.manualColumnPositions || instance.manualColumnPositions.length === 0) { + if (!instance.manualColumnPositions) { + instance.manualColumnPositions = []; + } + } + if (col) { + if (!instance.manualColumnPositions[col]) { + createPositionData(col + 1); + } + } else { + createPositionData(instance.countCols()); + } + } + + /** + * Fills the 'manualColumnPositions' array with consecutive column indexes + * @param {Number} len + */ + function createPositionData(len) { + if (instance.manualColumnPositions.length < len) { + for (var i = instance.manualColumnPositions.length; i < len; i++) { + instance.manualColumnPositions[i] = i; + } + } + } + + /** + * Updates the column order array used by modifyCol callback + * @param {Number} col + * @param {Number} actualCol column index of the currently selected cell + * @param {Number|null} returnCol suggested return slot for the unfreezed column (can be null) + * @param {String} action 'freeze' or 'unfreeze' + */ + function modifyColumnOrder(col, actualCol, returnCol, action) { + if (returnCol == null) { + returnCol = col; + } + + if (action === 'freeze') { + instance.manualColumnPositions.splice(fixedColumnsCount, 0, instance.manualColumnPositions.splice(actualCol, 1)[0]); + } else if (action === 'unfreeze') { + instance.manualColumnPositions.splice(returnCol, 0, instance.manualColumnPositions.splice(actualCol, 1)[0]); + } + } + + /** + * Estimates the most fitting return position for unfreezed column + * @param {Number} col + */ + function getBestColumnReturnPosition(col) { + var i = fixedColumnsCount, + j = getModifiedColumnIndex(i), + initialCol = getModifiedColumnIndex(col); + while (j < initialCol) { + i++; + j = getModifiedColumnIndex(i); + } + return i - 1; + } + + /** + * Freeze the given column (add it to fixed columns) + * @param {Number} col + */ + function freezeColumn(col) { + if (col <= fixedColumnsCount - 1) { + return; // already fixed + } + + var modifiedColumn = getModifiedColumnIndex(col) || col; + checkPositionData(modifiedColumn); + modifyColumnOrder(modifiedColumn, col, null, 'freeze'); + + addFixedColumn(); + } + + /** + * Unfreeze the given column (remove it from fixed columns and bring to it's previous position) + * @param {Number} col + */ + function unfreezeColumn(col) { + if (col > fixedColumnsCount - 1) { + return; // not fixed + } + + var returnCol = getBestColumnReturnPosition(col); + + var modifiedColumn = getModifiedColumnIndex(col) || col; + checkPositionData(modifiedColumn); + modifyColumnOrder(modifiedColumn, col, returnCol, 'unfreeze'); + removeFixedColumn(); + } + + function getModifiedColumnIndex(col) { + return instance.manualColumnPositions[col]; + } + + /** + * 'modiftyCol' callback + * @param {Number} col + */ + function onModifyCol(col) { + if (this.manualColumnPositionsPluginUsages.length > 1) { // if another plugin is using manualColumnPositions to modify column order, do not double the translation + return col; + } + return getModifiedColumnIndex(col); + } + + function bindHooks() { + //instance.addHook('afterGetColHeader', onAfterGetColHeader); + instance.addHook('modifyCol', onModifyCol); + instance.addHook('afterContextMenuDefaultOptions', addContextMenuEntry); + } + + return { + init: init, + freezeColumn: freezeColumn, + unfreezeColumn: unfreezeColumn, + helpers: { + addFixedColumn: addFixedColumn, + removeFixedColumn: removeFixedColumn, + checkPositionData: checkPositionData, + modifyColumnOrder: modifyColumnOrder, + getBestColumnReturnPosition: getBestColumnReturnPosition + } + }; + } + + var init = function init() { + if (!this.getSettings().manualColumnFreeze) { + return; + } + + var mcfPlugin; + + Handsontable.plugins.manualColumnFreeze = ManualColumnFreeze; + this.manualColumnFreeze = new ManualColumnFreeze(this); + + mcfPlugin = this.manualColumnFreeze; + mcfPlugin.init.call(this); + }; + + Handsontable.hooks.add('beforeInit', init); + +})(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); +}; + +WalkontableOverlay.prototype.makeClone = function (direction) { + var clone = document.createElement('DIV'); + clone.className = 'ht_clone_' + direction + ' handsontable'; + clone.style.position = 'absolute'; + clone.style.top = 0; + clone.style.left = 0; + 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.refresh = function (fastDraw) { + if (this.clone) { + this.clone.draw(fastDraw); + } +}; + +WalkontableOverlay.prototype.destroy = function () { + var eventManager = Handsontable.eventManager(this.clone); + + eventManager.clear(); +}; + +function WalkontableBorder(instance, settings) { + var style; + var createMultipleSelectorHandles = function () { + this.selectionHandles = { + topLeft: document.createElement('DIV'), + topLeftHitArea: document.createElement('DIV'), + bottomRight: document.createElement('DIV'), + bottomRightHitArea: document.createElement('DIV') + }; + var width = 10 + , hitAreaWidth = 40; + + this.selectionHandles.topLeft.className = 'topLeftSelectionHandle'; + this.selectionHandles.topLeftHitArea.className = 'topLeftSelectionHandle-HitArea'; + this.selectionHandles.bottomRight.className = 'bottomRightSelectionHandle'; + this.selectionHandles.bottomRightHitArea.className = 'bottomRightSelectionHandle-HitArea'; + + this.selectionHandles.styles = { + topLeft: this.selectionHandles.topLeft.style, + topLeftHitArea: this.selectionHandles.topLeftHitArea.style, + bottomRight: this.selectionHandles.bottomRight.style, + bottomRightHitArea: this.selectionHandles.bottomRightHitArea.style + }; + + var hitAreaStyle = { + 'position': 'absolute', + 'height': hitAreaWidth + 'px', + 'width': hitAreaWidth + 'px', + 'border-radius': parseInt(hitAreaWidth/1.5,10) + 'px' + }; + + for (var prop in hitAreaStyle) { + if (hitAreaStyle.hasOwnProperty(prop)) { + this.selectionHandles.styles.bottomRightHitArea[prop] = hitAreaStyle[prop]; + this.selectionHandles.styles.topLeftHitArea[prop] = hitAreaStyle[prop]; + } + } + + var handleStyle = { + 'position': 'absolute', + 'height': width + 'px', + 'width': width + 'px', + 'border-radius': parseInt(width/1.5,10) + 'px', + 'background': '#F5F5FF', + 'border': '1px solid #4285c8' + }; + + for (var prop in handleStyle) { + if (handleStyle.hasOwnProperty(prop)) { + this.selectionHandles.styles.bottomRight[prop] = handleStyle[prop]; + this.selectionHandles.styles.topLeft[prop] = handleStyle[prop]; + } + } + + this.main.appendChild(this.selectionHandles.topLeft); + this.main.appendChild(this.selectionHandles.bottomRight); + this.main.appendChild(this.selectionHandles.topLeftHitArea); + this.main.appendChild(this.selectionHandles.bottomRightHitArea); + }; + + if(!settings){ + return; + } + + var eventManager = Handsontable.eventManager(instance); + + //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.cornerDefaultStyle = { + width: '5px', + height: '5px', + borderWidth: '2px', + borderStyle: 'solid', + borderColor: '#FFF' + }; + + this.corner = this.main.childNodes[4]; + this.corner.className += ' corner'; + this.cornerStyle = this.corner.style; + this.cornerStyle.width = this.cornerDefaultStyle.width; + this.cornerStyle.height = this.cornerDefaultStyle.height; + this.cornerStyle.border = [ + this.cornerDefaultStyle.borderWidth, + this.cornerDefaultStyle.borderStyle, + this.cornerDefaultStyle.borderColor + ].join(' '); + + if(Handsontable.mobileBrowser) { + createMultipleSelectorHandles.call(this); + } + + 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; + + + + eventManager.addEventListener(document.body, 'mousedown', function () { + down = true; + }); + + + eventManager.addEventListener(document.body, 'mouseup', function () { + down = false; + }); + + /* jshint ignore:start */ + for (var c = 0, len = this.main.childNodes.length; c < len; c++) { + + eventManager.addEventListener(this.main.childNodes[c], 'mouseenter', function (event) { + if (!down || !instance.getSetting('hideBorderOnMouseDownOver')) { + return; + } + event.preventDefault(); + event.stopImmediatePropagation(); + + var bounds = this.getBoundingClientRect(); + + this.style.display = 'none'; + + 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; + } + }; + + var handler = function (event) { + if (isOutside(event)) { + eventManager.removeEventListener(document.body, 'mousemove', handler); + this.style.display = 'block'; + } + }; + eventManager.addEventListener(document.body, 'mousemove', handler); + }); + } + /* jshint ignore:end */ +} + +/** + * 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; + + var isPartRange = function () { + if(this.instance.selections.area.cellRange) { + + if (toRow != this.instance.selections.area.cellRange.to.row || + toColumn != this.instance.selections.area.cellRange.to.col) { + return true; + } + } + + return false; + }; + + var updateMultipleSelectionHandlesPosition = function (top, left, width, height) { + var handleWidth = parseInt(this.selectionHandles.styles.topLeft.width, 10) + , hitAreaWidth = parseInt(this.selectionHandles.styles.topLeftHitArea.width, 10); + + this.selectionHandles.styles.topLeft.top = parseInt(top - handleWidth,10) + "px"; + this.selectionHandles.styles.topLeft.left = parseInt(left - handleWidth,10) + "px"; + + this.selectionHandles.styles.topLeftHitArea.top = parseInt(top - (hitAreaWidth/4)*3,10) + "px"; + this.selectionHandles.styles.topLeftHitArea.left = parseInt(left - (hitAreaWidth/4)*3,10) + "px"; + + this.selectionHandles.styles.bottomRight.top = parseInt(top + height,10) + "px"; + this.selectionHandles.styles.bottomRight.left = parseInt(left + width,10) + "px"; + + this.selectionHandles.styles.bottomRightHitArea.top = parseInt(top + height - hitAreaWidth/4,10) + "px"; + this.selectionHandles.styles.bottomRightHitArea.left = parseInt(left + width - hitAreaWidth/4,10) + "px"; + + if(this.settings.border.multipleSelectionHandlesVisible && this.settings.border.multipleSelectionHandlesVisible()) { + this.selectionHandles.styles.topLeft.display = "block"; + this.selectionHandles.styles.topLeftHitArea.display = "block"; + if(!isPartRange.call(this)) { + this.selectionHandles.styles.bottomRight.display = "block"; + this.selectionHandles.styles.bottomRightHitArea.display = "block"; + } else { + this.selectionHandles.styles.bottomRight.display = "none"; + this.selectionHandles.styles.bottomRightHitArea.display = "none"; + } + } else { + this.selectionHandles.styles.topLeft.display = "none"; + this.selectionHandles.styles.bottomRight.display = "none"; + this.selectionHandles.styles.topLeftHitArea.display = "none"; + this.selectionHandles.styles.bottomRightHitArea.display = "none"; + } + + if(fromRow == this.instance.wtSettings.getSetting('fixedRowsTop') || fromColumn == this.instance.wtSettings.getSetting('fixedColumnsLeft')) { + this.selectionHandles.styles.topLeft.zIndex = "9999"; + this.selectionHandles.styles.topLeftHitArea.zIndex = "9999"; + } else { + this.selectionHandles.styles.topLeft.zIndex = ""; + this.selectionHandles.styles.topLeftHitArea.zIndex = ""; + } + + }; + + if (instance.cloneOverlay instanceof WalkontableVerticalScrollbarNative || instance.cloneOverlay instanceof WalkontableCornerScrollbarNative) { + ilen = instance.getSetting('fixedRowsTop'); + } + else { + ilen = instance.wtTable.getRenderedRowsCount(); + } + + for (i = 0; i < ilen; i++) { + s = instance.wtTable.rowFilter.renderedToSource(i); + if (s >= corners[0] && s <= corners[2]) { + fromRow = s; + break; + } + } + + for (i = ilen - 1; i >= 0; i--) { + s = instance.wtTable.rowFilter.renderedToSource(i); + if (s >= corners[0] && s <= corners[2]) { + toRow = s; + break; + } + } + + ilen = instance.wtTable.getRenderedColumnsCount(); + + for (i = 0; i < ilen; i++) { + s = instance.wtTable.columnFilter.renderedToSource(i); + if (s >= corners[1] && s <= corners[3]) { + fromColumn = s; + break; + } + } + + for (i = ilen - 1; i >= 0; i--) { + s = instance.wtTable.columnFilter.renderedToSource(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 (Handsontable.mobileBrowser || (!this.hasSetting(this.settings.border.cornerVisible) || isPartRange.call(this))) { + this.cornerStyle.display = 'none'; + } + else { + this.cornerStyle.top = top + height - 4 + 'px'; + this.cornerStyle.left = left + width - 4 + 'px'; + this.cornerStyle.borderRightWidth = this.cornerDefaultStyle.borderWidth; + this.cornerStyle.width = this.cornerDefaultStyle.width; + this.cornerStyle.display = 'block'; + + if (!instance.cloneOverlay && toColumn === instance.wtTable.getRenderedColumnsCount() - 1) { + var scrollableElement = Handsontable.Dom.getScrollableElement(instance.wtTable.TABLE), + needShrinkCorner = toTD.offsetLeft + Handsontable.Dom.outerWidth(toTD) >= Handsontable.Dom.innerWidth(scrollableElement); + + if (needShrinkCorner) { + this.cornerStyle.borderRightWidth = '0px'; + this.cornerStyle.width = Math.ceil(parseInt(this.cornerDefaultStyle.width, 10) / 2) + 'px'; + } + } + } + + if (Handsontable.mobileBrowser) { + updateMultipleSelectionHandlesPosition.call(this, top, left, width, height); + } +}; + +/** + * 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'; + + if(Handsontable.mobileBrowser) { + this.selectionHandles.styles.topLeft.display = 'none'; + this.selectionHandles.styles.bottomRight.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; +}; + +WalkontableCellCoords.prototype.isSouthWestOf = function (testedCoords) { + return this.row >= testedCoords.row && this.col <= testedCoords.col; +}; + +WalkontableCellCoords.prototype.isNorthEastOf = 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 selected range height (in number of rows) + * @returns {number} + */ +WalkontableCellRange.prototype.getHeight = function () { + return Math.max(this.from.row, this.to.row) - Math.min(this.from.row, this.to.row) + 1; +}; + +/** + * Returns selected range width (in number of columns) + * @returns {number} + */ +WalkontableCellRange.prototype.getWidth = function () { + return Math.max(this.from.col, this.to.col) - Math.min(this.from.col, this.to.col) + 1; +}; + +/** + * 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(); + + if (cellCoords.row < 0) { + cellCoords.row = 0; + } + + if (cellCoords.col < 0) { + cellCoords.col = 0; + } + + 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()); +}; + +WalkontableCellRange.prototype.isEqual = function (testedRange) { + return (Math.min(this.from.row, this.to.row) == Math.min(testedRange.from.row, testedRange.to.row)) && + (Math.max(this.from.row, this.to.row) == Math.max(testedRange.from.row, testedRange.to.row)) && + (Math.min(this.from.col, this.to.col) == Math.min(testedRange.from.col, testedRange.to.col)) && + (Math.max(this.from.col, this.to.col) == Math.max(testedRange.from.col, testedRange.to.col)); +}; + +/** + * 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() + , bottomRight = this.getBottomRightCorner() + , topRight = this.getTopRightCorner() + , bottomLeft = this.getBottomLeftCorner(); + + 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); + + var finalFrom = new WalkontableCellCoords(resultTopRow, resultTopCol) + , finalTo = new WalkontableCellCoords(resultBottomRow, resultBottomCol); + var isCorner = new WalkontableCellRange(finalFrom, finalFrom, finalTo).isCorner(this.from, expandingRange) + , onlyMerge = expandingRange.isEqual(new WalkontableCellRange(finalFrom, finalFrom, finalTo)); + + if (isCorner && !onlyMerge) { + if (this.from.col > finalFrom.col) { + finalFrom.col = resultBottomCol; + finalTo.col = resultTopCol; + } + if (this.from.row > finalFrom.row) { + finalFrom.row = resultBottomRow; + finalTo.row = resultTopRow; + } + } + + this.from = finalFrom; + this.to = finalTo; + + return true; +}; + +WalkontableCellRange.prototype.getDirection = function () { + if (this.from.isNorthWestOf(this.to)) { // NorthWest - SouthEast + return "NW-SE"; + } else if (this.from.isNorthEastOf(this.to)) { // NorthEast - SouthWest + return "NE-SW"; + } else if (this.from.isSouthEastOf(this.to)) { // SouthEast - NorthWest + return "SE-NW"; + } else if (this.from.isSouthWestOf(this.to)) { // SouthWest - NorthEast + return "SW-NE"; + } +}; + +WalkontableCellRange.prototype.setDirection = function (direction) { + switch (direction) { + case "NW-SE" : + this.from = this.getTopLeftCorner(); + this.to = this.getBottomRightCorner(); + break; + case "NE-SW" : + this.from = this.getTopRightCorner(); + this.to = this.getBottomLeftCorner(); + break; + case "SE-NW" : + this.from = this.getBottomRightCorner(); + this.to = this.getTopLeftCorner(); + break; + case "SW-NE" : + this.from = this.getBottomLeftCorner(); + this.to = this.getTopRightCorner(); + break; + } +}; + +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.getTopRightCorner = function () { + return new WalkontableCellCoords(Math.min(this.from.row, this.to.row), Math.max(this.from.col, this.to.col)); +}; + +WalkontableCellRange.prototype.getBottomLeftCorner = function () { + return new WalkontableCellCoords(Math.max(this.from.row, this.to.row), Math.min(this.from.col, this.to.col)); +}; + +WalkontableCellRange.prototype.isCorner = function (coords, expandedRange) { + if (expandedRange) { + if (expandedRange.includes(coords)) { + if (this.getTopLeftCorner().isEqual(new WalkontableCellCoords(expandedRange.from.row, expandedRange.from.col)) || + this.getTopRightCorner().isEqual(new WalkontableCellCoords(expandedRange.from.row, expandedRange.to.col)) || + this.getBottomLeftCorner().isEqual(new WalkontableCellCoords(expandedRange.to.row, expandedRange.from.col)) || + this.getBottomRightCorner().isEqual(new WalkontableCellCoords(expandedRange.to.row, expandedRange.to.col))) { + return true; + } + } + } + return coords.isEqual(this.getTopLeftCorner()) || coords.isEqual(this.getTopRightCorner()) || coords.isEqual(this.getBottomLeftCorner()) || coords.isEqual(this.getBottomRightCorner()); +}; + +WalkontableCellRange.prototype.getOppositeCorner = function (coords, expandedRange) { + if (!(coords instanceof WalkontableCellCoords)) { + return false; + } + + if (expandedRange) { + if (expandedRange.includes(coords)) { + if (this.getTopLeftCorner().isEqual(new WalkontableCellCoords(expandedRange.from.row, expandedRange.from.col))) { + return this.getBottomRightCorner(); + } + if (this.getTopRightCorner().isEqual(new WalkontableCellCoords(expandedRange.from.row, expandedRange.to.col))) { + return this.getBottomLeftCorner(); + } + if (this.getBottomLeftCorner().isEqual(new WalkontableCellCoords(expandedRange.to.row, expandedRange.from.col))) { + return this.getTopRightCorner(); + } + if (this.getBottomRightCorner().isEqual(new WalkontableCellCoords(expandedRange.to.row, expandedRange.to.col))) { + return this.getTopLeftCorner(); + } + } + } + + if (coords.isEqual(this.getBottomRightCorner())) { + return this.getTopLeftCorner(); + } else if (coords.isEqual(this.getTopLeftCorner())) { + return this.getBottomRightCorner(); + } else if (coords.isEqual(this.getTopRightCorner())) { + return this.getBottomLeftCorner(); + } else if (coords.isEqual(this.getBottomLeftCorner())) { + return this.getTopRightCorner(); + } +}; + +WalkontableCellRange.prototype.getBordersSharedWith = function (range) { + if (!this.includesRange(range)) { + return []; + } + + var thisBorders = { + top: Math.min(this.from.row, this.to.row), + bottom: Math.max(this.from.row, this.to.row), + left: Math.min(this.from.col, this.to.col), + right: Math.max(this.from.col, this.to.col) + } + , rangeBorders = { + top: Math.min(range.from.row, range.to.row), + bottom: Math.max(range.from.row, range.to.row), + left: Math.min(range.from.col, range.to.col), + right: Math.max(range.from.col, range.to.col) + } + , result = []; + + if (thisBorders.top == rangeBorders.top) { + result.push('top'); + } + if (thisBorders.right == rangeBorders.right) { + result.push('right'); + } + if (thisBorders.bottom == rangeBorders.bottom) { + result.push('bottom'); + } + if (thisBorders.left == rangeBorders.left) { + result.push('left'); + } + + return result; +}; + +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 + +/** + * WalkontableColumnFilter + * @constructor + */ +function WalkontableColumnFilter(offset,total, countTH) { + this.offset = offset; + this.total = total; + this.countTH = countTH; +} + +WalkontableColumnFilter.prototype.offsetted = function (n) { + return n + this.offset; +}; + +WalkontableColumnFilter.prototype.unOffsetted = function (n) { + return n - this.offset; +}; + +WalkontableColumnFilter.prototype.renderedToSource = function (n) { + return this.offsetted(n); +}; + +WalkontableColumnFilter.prototype.sourceToRendered = function (n) { + return this.unOffsetted(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.renderedToSource(this.offsettedTH(n)); +}; + +WalkontableColumnFilter.prototype.sourceColumnToVisibleRowHeadedColumn = function (n) { + return this.unOffsettedTH(this.sourceToRendered(n)); +}; + +/** + * WalkontableColumnStrategy + * @param containerSizeFn + * @param sizeAtIndex + * @param strategy - all, last, none + * @constructor + */ +function WalkontableColumnStrategy(instance, containerSizeFn, sizeAtIndex, strategy) { + var size + , i = 0; + + this.instance = instance; + 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.visibleCellCount++; + } + this.cellSizes.push(size); + this.cellSizesSum += size; + this.cellCount++; + + i++; + } + + var containerSize = this.getContainerSize(); + 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.getContainerSize = function () { + return typeof this.containerSizeFn === 'function' ? this.containerSizeFn() : this.containerSizeFn; +}; + +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() + , 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 && 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.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); + this.selections = this.getSetting('selections'); + + 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; +} + +/** + * Force rerender of Walkontable + * @param fastDraw {Boolean} When TRUE, try to refresh only the positions of borders without rerendering the data. + * It will only work if WalkontableTable.draw() does not force rendering anyway + * @returns {Walkontable} + */ +Walkontable.prototype.draw = function (fastDraw) { + this.drawInterrupted = false; + if (!fastDraw && !Handsontable.Dom.isVisible(this.wtTable.TABLE)) { + this.drawInterrupted = true; //draw interrupted because TABLE is not visible + return; + } + + this.wtTable.draw(fastDraw); + + return this; +}; + +/** + * Returns the TD at coords. If topmost is set to true, returns TD from the topmost overlay layer, + * if not set or set to false, returns TD from the master table. + * @param {WalkontableCellCoords} coords + * @param {Boolean} topmost + * @returns {Object} + */ +Walkontable.prototype.getCell = function (coords, topmost) { + if(!topmost) { + return this.wtTable.getCell(coords); + } else { + var fixedRows = this.wtSettings.getSetting('fixedRowsTop') + , fixedColumns = this.wtSettings.getSetting('fixedColumnsLeft'); + + if(coords.row < fixedRows && coords.col < fixedColumns) { + return this.wtScrollbars.corner.clone.wtTable.getCell(coords); + } else if(coords.row < fixedRows) { + return this.wtScrollbars.vertical.clone.wtTable.getCell(coords); + } else if (coords.col < fixedColumns) { + return this.wtScrollbars.horizontal.clone.wtTable.getCell(coords); + } else { + return this.wtTable.getCell(coords); + } + } +}; + +Walkontable.prototype.update = function (settings, value) { + return this.wtSettings.update(settings, value); +}; + +/** + * Scroll the viewport to a row at the given index in the data source + * @param row + * @returns {Walkontable} + */ +Walkontable.prototype.scrollVertical = function (row) { + this.wtScrollbars.vertical.scrollTo(row); + this.getSetting('onScrollVertically'); + return this; +}; + +/** + * Scroll the viewport to a column at the given index in the data source + * @param row + * @returns {Walkontable} + */ +Walkontable.prototype.scrollHorizontal = function (column) { + this.wtScrollbars.horizontal.scrollTo(column); + this.getSetting('onScrollHorizontally'); + return this; +}; + +/** + * 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/handsontable/wiki/JavaScript-&-DOM-performance-tips +}; + +Walkontable.prototype.hasSetting = function (key) { + return this.wtSettings.has(key); +}; + +Walkontable.prototype.destroy = function () { + this.wtScrollbars.destroy(); + + if ( 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; + + Handsontable.Dom.addClass(this.clone.wtTable.holder.parentNode, 'wtDebugVisible'); + + /*var that = this; + var lastX = 0; + var lastY = 0; + var overlayContainer = that.clone.wtTable.holder.parentNode; + + var eventManager = Handsontable.eventManager(instance); + + eventManager.addEventListener(document.body, 'mousemove', 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.destroy = function () { + WalkontableOverlay.prototype.destroy.call(this); + clearTimeout(this.lastTimeout); +}; + +function WalkontableEvent(instance) { + var that = this; + + var eventManager = Handsontable.eventManager(instance); + + //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 onTouchMove = function (event) { + that.instance.touchMoving = true; + }; + + var longTouchTimeout; + + ///** + // * Update touch event target - if user taps on resize handle 'hit area', update the target to the cell itself + // * @param event + // */ + /* + var adjustTapTarget = function (event) { + var currentSelection + , properTarget; + + if(Handsontable.Dom.hasClass(event.target,'SelectionHandle')) { + if(that.instance.selections[0].cellRange) { + currentSelection = that.instance.selections[0].cellRange.highlight; + + properTarget = that.instance.getCell(currentSelection, true); + } + } + + if(properTarget) { + Object.defineProperty(event,'target',{ + value: properTarget + }); + } + + return event; + };*/ + + var onTouchStart = function (event) { + var container = this; + + eventManager.addEventListener(this, 'touchmove', onTouchMove); + + //this.addEventListener("touchmove", onTouchMove, false); + + // touch-and-hold event + //longTouchTimeout = setTimeout(function () { + // if(!that.instance.touchMoving) { + // that.instance.longTouch = true; + // + // var targetCoords = Handsontable.Dom.offset(event.target); + // var contextMenuEvent = new MouseEvent('contextmenu', { + // clientX: targetCoords.left + event.target.offsetWidth, + // clientY: targetCoords.top + event.target.offsetHeight, + // button: 2 + // }); + // + // that.instance.wtTable.holder.parentNode.parentNode.dispatchEvent(contextMenuEvent); + // } + //},200); + + // Prevent cell selection when scrolling with touch event - not the best solution performance-wise + that.checkIfTouchMove = setTimeout(function () { + if (that.instance.touchMoving === true) { + that.instance.touchMoving = void 0; + + eventManager.removeEventListener("touchmove", onTouchMove, false); + + return; + } else { + //event = adjustTapTarget(event); + + onMouseDown(event); + } + }, 30); + + //eventManager.removeEventListener(that.instance.wtTable.holder, "mousedown", onMouseDown); + }; + + 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); + } + } + }; + + + var onTouchEnd = function (event) { + clearTimeout(longTouchTimeout); + //that.instance.longTouch == void 0; + + event.preventDefault(); + + onMouseUp(event); + + //eventManager.removeEventListener(that.instance.wtTable.holder, "mouseup", onMouseUp); + }; + + eventManager.addEventListener(this.instance.wtTable.holder, 'mousedown', onMouseDown); + + eventManager.addEventListener(this.instance.wtTable.TABLE, 'mouseover', onMouseOver); + + eventManager.addEventListener(this.instance.wtTable.holder, 'mouseup', onMouseUp); + + + if(this.instance.wtTable.holder.parentNode.parentNode && Handsontable.mobileBrowser) { // check if full HOT instance, or detached WOT AND run on mobile device + var classSelector = "." + this.instance.wtTable.holder.parentNode.className.split(" ").join("."); + + eventManager.addEventListener(this.instance.wtTable.holder.parentNode.parentNode, 'touchstart', function (event) { + that.instance.touchApplied = true; + if (Handsontable.Dom.isChildOf(event.target, classSelector)) { + onTouchStart.call(event.target, event); + } + }); + eventManager.addEventListener(this.instance.wtTable.holder.parentNode.parentNode, 'touchend', function (event) { + that.instance.touchApplied = false; + if (Handsontable.Dom.isChildOf(event.target, classSelector)) { + onTouchEnd.call(event.target, event); + } + }); + + if(!that.instance.momentumScrolling) { + that.instance.momentumScrolling = {}; + } + eventManager.addEventListener(this.instance.wtTable.holder.parentNode.parentNode, 'scroll', function (event) { + clearTimeout(that.instance.momentumScrolling._timeout); + + if(!that.instance.momentumScrolling.ongoing) { + that.instance.getSetting('onBeforeTouchScroll'); + } + that.instance.momentumScrolling.ongoing = true; + + that.instance.momentumScrolling._timeout = setTimeout(function () { + if(!that.instance.touchApplied) { + that.instance.momentumScrolling.ongoing = false; + + that.instance.getSetting('onAfterMomentumScroll'); + } + },200); + }); + } + + eventManager.addEventListener(window, 'resize', function () { + that.instance.draw(); + }); + + this.destroy = function () { + clearTimeout(this.dblClickTimeout[0]); + clearTimeout(this.dblClickTimeout[1]); + + eventManager.clear(); + }; +} + +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; //selections.current is current selected cell + cell.TD = this.instance.wtTable.getCell(cell.coords); + } else if (Handsontable.Dom.hasClass(elem, 'wtBorder') && Handsontable.Dom.hasClass(elem, 'area')) { + if (this.instance.selections.area.cellRange) { + cell.coords = this.instance.selections.area.cellRange.to; //selections.area is area selected cells + 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; +} + +/** + * 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; + /* jshint -W121 */ + 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.renderedToSource = function (n) { + return this.offsetted(n); +}; + +WalkontableRowFilter.prototype.sourceToRendered = function (n) { + return this.unOffsetted(n); +}; + +WalkontableRowFilter.prototype.offsettedTH = function (n) { + return n - this.countTH; +}; + +WalkontableRowFilter.prototype.visibleColHeadedRowToSourceRow = function (n) { + return this.renderedToSource(this.offsettedTH(n)); +}; + +WalkontableRowFilter.prototype.sourceRowToVisibleColHeadedRow = function (n) { + return this.unOffsettedTH(this.sourceToRendered(n)); +}; + +function WalkontableScroll(instance) { + this.instance = instance; +} + +/** + * Scrolls viewport to a cell by minimum number of cells + * @param {WalkontableCellCoords} coords + */ +WalkontableScroll.prototype.scrollViewport = function (coords) { + if (!this.instance.drawn) { + return; + } + + var 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'); + } + + if (coords.row > this.instance.wtTable.getLastVisibleRow()) { + this.instance.wtScrollbars.vertical.scrollTo(coords.row, true); + } else if (coords.row >= this.instance.getSetting('fixedRowsTop') && coords.row < this.instance.wtTable.getFirstVisibleRow()){ + this.instance.wtScrollbars.vertical.scrollTo(coords.row); + } + + if (coords.col > this.instance.wtTable.getLastVisibleColumn()) { + this.instance.wtScrollbars.horizontal.scrollTo(coords.col, true); + } else if (coords.col > this.instance.getSetting('fixedColumnsLeft') && coords.col < this.instance.wtTable.getFirstVisibleColumn()){ + this.instance.wtScrollbars.horizontal.scrollTo(coords.col); + } + + //} +}; + +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, + finalLeft, + finalTop; + + if (this.scrollHandler === window) { + var box = this.instance.wtTable.holder.getBoundingClientRect(); + var top = Math.ceil(box.top); + var left = Math.ceil(box.left); + var bottom = Math.ceil(box.bottom); + var right = Math.ceil(box.right); + + if (left < 0 && (right - elem.offsetWidth) > 0) { + finalLeft = -left + 'px'; + } else { + finalLeft = '0'; + } + + if (top < 0 && (bottom - elem.offsetHeight) > 0) { + finalTop = -top + "px"; + } else { + finalTop = "0"; + } + } + else if(!Handsontable.freezeOverlays) { + finalLeft = this.instance.wtScrollbars.horizontal.getScrollPosition() + "px"; + finalTop = this.instance.wtScrollbars.vertical.getScrollPosition() + "px"; + } + + Handsontable.Dom.setOverlayPosition(elem, finalLeft, finalTop); + + elem.style.width = Handsontable.Dom.outerWidth(this.clone.wtTable.TABLE) + 4 + 'px'; + elem.style.height = Handsontable.Dom.outerHeight(this.clone.wtTable.TABLE) + 4 + 'px'; +}; + +function WalkontableHorizontalScrollbarNative(instance) { + this.instance = instance; + this.type = 'horizontal'; + 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 () { + var finalLeft, finalTop; + + 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); + var right = Math.ceil(box.right); + + if (left < 0 && (right - elem.offsetWidth) > 0) { + finalLeft = -left + 'px'; + } else { + finalLeft = '0'; + } + + finalTop = this.instance.wtTable.hider.style.top; + } + else if(!Handsontable.freezeOverlays) { + finalLeft = this.getScrollPosition() + "px"; + finalTop = this.instance.wtTable.hider.style.top; + } + + Handsontable.Dom.setOverlayPosition(elem, finalLeft, finalTop); + + elem.style.height = Handsontable.Dom.outerHeight(this.clone.wtTable.TABLE) + 'px'; + elem.style.width = Handsontable.Dom.outerWidth(this.clone.wtTable.TABLE) + 4 + 'px';// + 4 + 'px'; +}; + +WalkontableHorizontalScrollbarNative.prototype.refresh = function (fastDraw) { + this.applyToDOM(); + WalkontableOverlay.prototype.refresh.call(this, fastDraw); +}; + +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 () { + this.instance.getSetting('onScrollHorizontally'); +}; + +WalkontableHorizontalScrollbarNative.prototype.sumCellSizes = function (from, length) { + var sum = 0; + while(from < length) { + sum += this.instance.wtTable.getStretchedColumnWidth(from) || this.instance.wtSettings.defaultColumnWidth; + from++; + } + return sum; +}; + +//applyToDOM (in future merge it with this.refresh?) +WalkontableHorizontalScrollbarNative.prototype.applyToDOM = function () { + var total = this.instance.getSetting('totalColumns'); + var headerSize = this.instance.wtViewport.getRowHeaderWidth(); + + this.fixedContainer.style.width = headerSize + this.sumCellSizes(0, total) + 'px';// + 4 + 'px'; + + if (typeof this.instance.wtViewport.columnsRenderCalculator.startPosition === 'number'){ + this.fixed.style.left = this.instance.wtViewport.columnsRenderCalculator.startPosition + 'px'; + } + else if (total === 0) { + this.fixed.style.left = '0'; + } else { + throw new Error('Incorrect value of the columnsRenderCalculator'); + } + this.fixed.style.right = ''; +}; + +/** + * Scrolls horizontally to a column at the left edge of the viewport + * @param sourceCol {Number} + * @param beyondRendered {Boolean} if TRUE, scrolls according to the bottom edge (top edge is by default) + */ +WalkontableHorizontalScrollbarNative.prototype.scrollTo = function (sourceCol, beyondRendered) { + var newX = this.getTableParentOffset(); + + if (beyondRendered) { + newX += this.sumCellSizes(0, sourceCol + 1); + newX -= this.instance.wtViewport.getViewportWidth(); + } + else { + var fixedColumnsLeft = this.instance.getSetting('fixedColumnsLeft'); + newX += this.sumCellSizes(fixedColumnsLeft, sourceCol); + } + + this.setScrollPosition(newX); +}; + +WalkontableHorizontalScrollbarNative.prototype.getTableParentOffset = function () { + if (this.scrollHandler === window) { + return this.instance.wtTable.holderOffset.left; + } + else { + return 0; + } +}; + +function WalkontableVerticalScrollbarNative(instance) { + this.instance = instance; + this.type = 'vertical'; + this.init(); + this.clone = this.makeClone('top'); +} + +WalkontableVerticalScrollbarNative.prototype = new WalkontableOverlay(); + +//resetFixedPosition (in future merge it with this.refresh?) +WalkontableVerticalScrollbarNative.prototype.resetFixedPosition = function () { + var finalLeft, finalTop; + + 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 bottom = Math.ceil(box.bottom); + + finalLeft = this.instance.wtTable.hider.style.left; + + if (top < 0 && (bottom - elem.offsetHeight) > 0) { + finalTop = -top + "px"; + } else { + finalTop = "0"; + } + } + else if(!Handsontable.freezeOverlays) { + finalTop = this.getScrollPosition() + "px"; + finalLeft = this.instance.wtTable.hider.style.left; + } + + Handsontable.Dom.setOverlayPosition(elem, finalLeft, finalTop); + + 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';// + 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 () { + this.instance.getSetting('onScrollVertically'); +}; + +WalkontableVerticalScrollbarNative.prototype.sumCellSizes = function (from, length) { + var sum = 0; + while (from < length) { + sum += this.instance.wtTable.getRowHeight(from) || this.instance.wtSettings.settings.defaultRowHeight; //TODO optimize getSetting, because this is MUCH faster then getSetting + from++; + } + return sum; +}; + +WalkontableVerticalScrollbarNative.prototype.refresh = function (fastDraw) { + this.applyToDOM(); + WalkontableOverlay.prototype.refresh.call(this, fastDraw); +}; + +//applyToDOM (in future merge it with this.refresh?) +WalkontableVerticalScrollbarNative.prototype.applyToDOM = function () { + var total = this.instance.getSetting('totalRows'); + var headerSize = this.instance.wtViewport.getColumnHeaderHeight(); + + this.fixedContainer.style.height = headerSize + this.sumCellSizes(0, total) + 'px'; + if (typeof this.instance.wtViewport.rowsRenderCalculator.startPosition === 'number') { + this.fixed.style.top = this.instance.wtViewport.rowsRenderCalculator.startPosition + 'px'; + } + else if (total === 0) { + this.fixed.style.top = '0'; //can happen if there are 0 rows + } + else { + throw new Error("Incorrect value of the rowsRenderCalculator"); + } + this.fixed.style.bottom = ''; +}; + +/** + * Scrolls vertically to a row + * + * @param sourceRow {Number} + * @param bottomEdge {Boolean} if TRUE, scrolls according to the bottom edge (top edge is by default) + */ +WalkontableVerticalScrollbarNative.prototype.scrollTo = function (sourceRow, bottomEdge) { + var newY = this.getTableParentOffset(); + + if (bottomEdge) { + newY += this.sumCellSizes(0, sourceRow + 1); + newY -= this.instance.wtViewport.getViewportHeight(); + // Fix 1 pixel offset when cell is selected + newY += 1; + } + else { + var fixedRowsTop = this.instance.getSetting('fixedRowsTop'); + newY += this.sumCellSizes(fixedRowsTop, sourceRow); + } + + this.setScrollPosition(newY); +}; + +WalkontableVerticalScrollbarNative.prototype.getTableParentOffset = function () { + if (this.scrollHandler === window) { + return this.instance.wtTable.holderOffset.top; + } + else { + return 0; + } +}; + +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; + + this.refreshAll = 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.instance.draw(true); + that.vertical.onScroll(); + that.horizontal.onScroll(); + }; + + var eventManager = Handsontable.eventManager(that.instance); + + eventManager.addEventListener(this.vertical.scrollHandler, 'scroll', this.refreshAll); + if (this.vertical.scrollHandler !== this.horizontal.scrollHandler) { + eventManager.addEventListener(this.horizontal.scrollHandler, 'scroll', this.refreshAll); + } + + if (this.vertical.scrollHandler !== window && this.horizontal.scrollHandler !== window) { + eventManager.addEventListener(window,'scroll', this.refreshAll); + } +}; + +WalkontableScrollbars.prototype.destroy = function () { + var eventManager = Handsontable.eventManager(this.instance); + + if (this.vertical) { + this.vertical.destroy(); + eventManager.removeEventListener(this.vertical.scrollHandler,'scroll', this.refreshAll); + } + if (this.horizontal) { + this.horizontal.destroy(); + eventManager.removeEventListener(this.horizontal.scrollHandler,'scroll', this.refreshAll); + } + eventManager.removeEventListener(window,'scroll', this.refreshAll); + if (this.corner ) { + this.corner.destroy(); + } + if (this.debug) { + this.debug.destroy(); + } +}; + +WalkontableScrollbars.prototype.refresh = function (fastDraw) { + if (this.horizontal) { + this.horizontal.refresh(fastDraw); + } + if (this.vertical) { + this.vertical.refresh(fastDraw); + } + if (this.corner) { + this.corner.refresh(fastDraw); + } + if (this.debug) { + this.debug.refresh(fastDraw); + } +}; + +WalkontableScrollbars.prototype.applyToDOM = function () { + if (this.horizontal) { + this.horizontal.applyToDOM(); + } + if (this.vertical) { + this.vertical.applyToDOM(); + } +}; + +function WalkontableSelection(settings, cellRange) { + this.settings = settings; + this.cellRange = cellRange || null; + this.instanceBorders = {}; +} + +/** + * Each Walkontable clone requires it's own border for every selection. This method creates and returns selection borders per instance + * @param {Walkontable} instance + * @returns {WalkontableBorder} + */ +WalkontableSelection.prototype.getBorder = function (instance) { + if (this.instanceBorders[instance.guid]) { + return this.instanceBorders[instance.guid]; + } + //where is this returned? + this.instanceBorders[instance.guid] = new WalkontableBorder(instance, this.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(), + bottomRight = this.cellRange.getBottomRightCorner(); + + return [topLeft.row, topLeft.col, bottomRight.row, bottomRight.col]; +}; + +WalkontableSelection.prototype.addClassAtCoords = function (instance, sourceRow, sourceColumn, cls) { + var TD = instance.wtTable.getCell(new WalkontableCellCoords(sourceRow, sourceColumn)); + + if (typeof TD === 'object') { + Handsontable.Dom.addClass(TD, cls); + } +}; + +WalkontableSelection.prototype.draw = function (instance) { + var + _this = this, + renderedRows = instance.wtTable.getRenderedRowsCount(), + renderedColumns = instance.wtTable.getRenderedColumnsCount(), + corners, sourceRow, sourceCol, border, TH; + + if (this.isEmpty()) { + if (this.settings.border) { + border = this.getBorder(instance); + + if (border) { + border.disappear(); + } + } + + return; + } + + corners = this.getCorners(); + + for (var column = 0; column < renderedColumns; column++) { + sourceCol = instance.wtTable.columnFilter.renderedToSource(column); + + if (sourceCol >= corners[1] && sourceCol <= corners[3]) { + TH = instance.wtTable.getColumnHeader(sourceCol); + + if (TH && _this.settings.highlightColumnClassName) { + Handsontable.Dom.addClass(TH, _this.settings.highlightColumnClassName); + } + } + } + + for (var row = 0; row < renderedRows; row++) { + sourceRow = instance.wtTable.rowFilter.renderedToSource(row); + + if (sourceRow >= corners[0] && sourceRow <= corners[2]) { + TH = instance.wtTable.getRowHeader(sourceRow); + + if (TH && _this.settings.highlightRowClassName) { + Handsontable.Dom.addClass(TH, _this.settings.highlightRowClassName); + } + } + + for (var column = 0; column < renderedColumns; column++) { + sourceCol = instance.wtTable.columnFilter.renderedToSource(column); + + if (sourceRow >= corners[0] && sourceRow <= corners[2] && sourceCol >= corners[1] && sourceCol <= corners[3]) { + // selected cell + if (_this.settings.className) { + _this.addClassAtCoords(instance, sourceRow, sourceCol, _this.settings.className); + } + } + else if (sourceRow >= corners[0] && sourceRow <= corners[2]) { + // selection is in this row + if (_this.settings.highlightRowClassName) { + _this.addClassAtCoords(instance, sourceRow, sourceCol, _this.settings.highlightRowClassName); + } + } + else if (sourceCol >= corners[1] && sourceCol <= corners[3]) { + // selection is in this column + if (_this.settings.highlightColumnClassName) { + _this.addClassAtCoords(instance, sourceRow, sourceCol, _this.settings.highlightColumnClassName); + } + } + } + } + + instance.getSetting('onBeforeDrawBorders', corners, this.settings.className); + + if (this.settings.border) { + border = this.getBorder(instance); + + if (border) { + // warning! border.appear modifies corners! + border.appear(corners); + } + } +}; + +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, + 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, + cellRenderer: function (row, column, TD) { + var cellData = that.getSetting('data', row, column); + Handsontable.Dom.fastInnerText(TD, cellData === void 0 || cellData === null ? '' : cellData); + }, + //columnWidth: 50, + columnWidth: function (col) { + return; //return undefined means use default size for the rendered cell content + }, + rowHeight: function (row) { + return; //return undefined means use default size for the rendered cell content + }, + defaultRowHeight: 23, + defaultColumnWidth: 50, + selections: null, + hideBorderOnMouseDownOver: false, + viewportRowCalculatorOverride: null, + viewportColumnCalculatorOverride: null, + + //callbacks + onCellMouseDown: null, + onCellMouseOver: null, +// onCellMouseOut: null, + onCellDblClick: null, + onCellCornerMouseDown: null, + onCellCornerDblClick: null, + beforeDraw: null, + onDraw: null, + onBeforeDrawBorders: null, + onScrollVertically: null, + onScrollHorizontally: null, + onBeforeTouchScroll: null, + onAfterMomentumScroll: null, + + //constants + scrollbarWidth: 10, + scrollbarHeight: 10, + + renderAllRows: false, + groups: 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/handsontable/wiki/JavaScript-&-DOM-performance-tips + } + else if (param1 !== void 0 && Array.isArray(this.settings[key])) { //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; + + if (!this.isWorkingOnClone()) { + this.holder.parentNode.style.position = "relative"; + } + + //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.rowFilter = null; + this.columnFilter = null; +} + +WalkontableTable.prototype.isWorkingOnClone = function () { + return !!this.instance.cloneSource; +}; + +/** + * Redraws the table + * @param fastDraw {Boolean} If TRUE, will try to avoid full redraw and only update the border positions. If FALSE or UNDEFINED, will perform a full redraw + * @returns {WalkontableTable} + */ +WalkontableTable.prototype.draw = function (fastDraw) { + if (!this.isWorkingOnClone()) { + this.holderOffset = Handsontable.Dom.offset(this.holder); + fastDraw = this.instance.wtViewport.createRenderCalculators(fastDraw); + } + + if (!fastDraw) { + if (this.isWorkingOnClone()) { + this.tableOffset = this.instance.cloneSource.wtTable.tableOffset; + } + else { + this.tableOffset = Handsontable.Dom.offset(this.TABLE); + } + var startRow; + if (this.instance.cloneOverlay instanceof WalkontableDebugOverlay || + this.instance.cloneOverlay instanceof WalkontableVerticalScrollbarNative || + this.instance.cloneOverlay instanceof WalkontableCornerScrollbarNative) { + startRow = 0; + } + else { + startRow = this.instance.wtViewport.rowsRenderCalculator.startRow; + } + + + var startColumn; + if (this.instance.cloneOverlay instanceof WalkontableDebugOverlay || + this.instance.cloneOverlay instanceof WalkontableHorizontalScrollbarNative || + this.instance.cloneOverlay instanceof WalkontableCornerScrollbarNative) { + startColumn = 0; + } else { + startColumn = this.instance.wtViewport.columnsRenderCalculator.startColumn; + } + + this.rowFilter = new WalkontableRowFilter( + startRow, + this.instance.getSetting('totalRows'), + this.instance.getSetting('columnHeaders').length + ); + this.columnFilter = new WalkontableColumnFilter( + startColumn, + this.instance.getSetting('totalColumns'), + this.instance.getSetting('rowHeaders').length + ); + this._doDraw(); //creates calculator after draw + } + else { + if (!this.isWorkingOnClone()) { + //in case we only scrolled without redraw, update visible rows information in oldRowsCalculator + this.instance.wtViewport.createVisibleCalculators(); + } + if (this.instance.wtScrollbars) { + this.instance.wtScrollbars.refresh(true); + } + } + + this.refreshSelections(fastDraw); + + if (!this.isWorkingOnClone()) { + this.instance.wtScrollbars.vertical.resetFixedPosition(); + this.instance.wtScrollbars.horizontal.resetFixedPosition(); + this.instance.wtScrollbars.corner.resetFixedPosition(); + } + + this.instance.drawn = true; + return this; +}; + +WalkontableTable.prototype._doDraw = function () { + var wtRenderer = new WalkontableTableRenderer(this); + wtRenderer.render(); +}; + +WalkontableTable.prototype.removeClassFromCells = function (className) { + var nodes = this.TABLE.querySelectorAll('.' + className); + for (var i = 0, ilen = nodes.length; i < ilen; i++) { + Handsontable.Dom.removeClass(nodes[i], className); + } +}; + +WalkontableTable.prototype.refreshSelections = function (fastDraw) { + var i, len; + + if (!this.instance.selections) { + return; + } + len = this.instance.selections.length; + + if (fastDraw) { + for (i = 0; i < len; i++) { + // there was no rerender, so we need to remove classNames by ourselves + if (this.instance.selections[i].settings.className) { + this.removeClassFromCells(this.instance.selections[i].settings.className); + } + if (this.instance.selections[i].settings.highlightRowClassName) { + this.removeClassFromCells(this.instance.selections[i].settings.highlightRowClassName); + } + if (this.instance.selections[i].settings.highlightColumnClassName) { + this.removeClassFromCells(this.instance.selections[i].settings.highlightColumnClassName); + } + } + } + for (i = 0; i < len; i++) { + this.instance.selections[i].draw(this.instance, fastDraw); + } +}; + +/** + * 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.isRowBeforeRenderedRows(coords.row)) { + return -1; //row before rendered rows + } + else if (this.isRowAfterRenderedRows(coords.row)) { + return -2; //row after rendered rows + } + + var TR = this.TBODY.childNodes[this.rowFilter.sourceToRendered(coords.row)]; + + if (TR) { + return TR.childNodes[this.columnFilter.sourceColumnToVisibleRowHeadedColumn(coords.col)]; + } +}; + +/** + * getColumnHeader + * @param col + * @param level Header level (0 = most distant to the table) + * @return {Object} HTMLElement on success or undefined on error + * + */ +WalkontableTable.prototype.getColumnHeader = function(col, level) { + if(!level) { + level = 0; + } + + var TR = this.THEAD.childNodes[level]; + if (TR) { + return TR.childNodes[this.columnFilter.sourceColumnToVisibleRowHeadedColumn(col)]; + } +}; + +/** + * getRowHeader + * @param row + * @return {Object} HTMLElement on success or {Number} one of the exit codes on error: + * null table doesn't have row headers + * + */ +WalkontableTable.prototype.getRowHeader = function(row) { + if(this.columnFilter.sourceColumnToVisibleRowHeadedColumn(0) === 0) { + return null; + } + + var TR = this.TBODY.childNodes[this.rowFilter.sourceToRendered(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.renderedToSource(row); + } + + return new WalkontableCellCoords( + row, + this.columnFilter.visibleRowHeadedColumnToSourceColumn(TD.cellIndex) + ); +}; + +WalkontableTable.prototype.getTrForRow = function (row) { + return this.TBODY.childNodes[this.rowFilter.sourceToRendered(row)]; +}; + +WalkontableTable.prototype.getFirstRenderedRow = function () { + return this.instance.wtViewport.rowsRenderCalculator.startRow; +}; + +WalkontableTable.prototype.getFirstVisibleRow = function () { + return this.instance.wtViewport.rowsVisibleCalculator.startRow; +}; + +WalkontableTable.prototype.getFirstRenderedColumn = function () { + return this.instance.wtViewport.columnsRenderCalculator.startColumn; +}; + +//returns -1 if no column is visible +WalkontableTable.prototype.getFirstVisibleColumn = function () { + return this.instance.wtViewport.columnsVisibleCalculator.startColumn; +}; + +//returns -1 if no row is visible +WalkontableTable.prototype.getLastRenderedRow = function () { + return this.instance.wtViewport.rowsRenderCalculator.endRow; +}; + +WalkontableTable.prototype.getLastVisibleRow = function () { + return this.instance.wtViewport.rowsVisibleCalculator.endRow; +}; + +WalkontableTable.prototype.getLastRenderedColumn = function () { + return this.instance.wtViewport.columnsRenderCalculator.endColumn; +}; + +//returns -1 if no column is visible +WalkontableTable.prototype.getLastVisibleColumn = function () { + return this.instance.wtViewport.columnsVisibleCalculator.endColumn; +}; + +WalkontableTable.prototype.isRowBeforeRenderedRows = function (r) { + return (this.rowFilter.sourceToRendered(r) < 0 && r >= 0); +}; + +WalkontableTable.prototype.isRowAfterViewport = function (r) { + return (r > this.getLastVisibleRow()); +}; + +WalkontableTable.prototype.isRowAfterRenderedRows = function (r) { + return (r > this.getLastRenderedRow()); +}; + +WalkontableTable.prototype.isColumnBeforeViewport = function (c) { + return (this.columnFilter.sourceToRendered(c) < 0 && c >= 0); +}; + +WalkontableTable.prototype.isColumnAfterViewport = function (c) { + return (c > this.getLastVisibleColumn()); +}; + +WalkontableTable.prototype.isLastRowFullyVisible = function () { + return (this.getLastVisibleRow() === this.getLastRenderedRow()); +}; + +WalkontableTable.prototype.isLastColumnFullyVisible = function () { + return (this.getLastVisibleColumn() === this.getLastRenderedColumn); +}; + +WalkontableTable.prototype.getRenderedColumnsCount = function () { + if (this.instance.cloneOverlay instanceof WalkontableDebugOverlay) { + return this.instance.getSetting('totalColumns'); + } + else if (this.instance.cloneOverlay instanceof WalkontableHorizontalScrollbarNative || this.instance.cloneOverlay instanceof WalkontableCornerScrollbarNative) { + return this.instance.getSetting('fixedColumnsLeft'); + } + else { + return this.instance.wtViewport.columnsRenderCalculator.count; + } +}; + +WalkontableTable.prototype.getRenderedRowsCount = function () { + if (this.instance.cloneOverlay instanceof WalkontableDebugOverlay) { + return this.instance.getSetting('totalRows'); + } + else if (this.instance.cloneOverlay instanceof WalkontableVerticalScrollbarNative || this.instance.cloneOverlay instanceof WalkontableCornerScrollbarNative) { + return this.instance.getSetting('fixedRowsTop'); + } + return this.instance.wtViewport.rowsRenderCalculator.count; +}; + +WalkontableTable.prototype.getVisibleRowsCount = function () { + return this.instance.wtViewport.rowsVisibleCalculator.count; +}; + +WalkontableTable.prototype.allRowsInViewport = function () { + return this.instance.getSetting('totalRows') == this.getVisibleRowsCount(); +}; + +/** + * Checks if any of the row's cells content exceeds its initial height, and if so, returns the oversized height + * @param {Number} sourceRow + * @return {Number} + */ +WalkontableTable.prototype.getRowHeight = function (sourceRow) { + var height = this.instance.wtSettings.settings.rowHeight(sourceRow); + var oversizedHeight = this.instance.wtViewport.oversizedRows[sourceRow]; + if (oversizedHeight !== void 0) { + height = height ? Math.max(height, oversizedHeight) : oversizedHeight; + } + return height; +}; + +WalkontableTable.prototype.getColumnHeaderHeight = function (level) { + var height = this.instance.wtSettings.settings.defaultRowHeight, + oversizedHeight = this.instance.wtViewport.oversizedColumnHeaders[level]; + if (oversizedHeight !== void 0) { + height = height ? Math.max(height, oversizedHeight) : oversizedHeight; + } + return height; +}; + +WalkontableTable.prototype.getVisibleColumnsCount = function () { + return this.instance.wtViewport.columnsVisibleCalculator.count; +}; + + +WalkontableTable.prototype.allColumnsInViewport = function () { + return this.instance.getSetting('totalColumns') == this.getVisibleColumnsCount(); +}; + + + +WalkontableTable.prototype.getColumnWidth = function (sourceColumn) { + var width = this.instance.wtSettings.settings.columnWidth; + if(typeof width === 'function') { + width = width(sourceColumn); + } else if(typeof width === 'object') { + width = width[sourceColumn]; + } + + var oversizedWidth = this.instance.wtViewport.oversizedCols[sourceColumn]; + if (oversizedWidth !== void 0) { + width = width ? Math.max(width, oversizedWidth) : oversizedWidth; + } + return width; +}; + +WalkontableTable.prototype.getStretchedColumnWidth = function (sourceColumn) { + var + width = this.getColumnWidth(sourceColumn) || this.instance.wtSettings.settings.defaultColumnWidth, + calculator = this.instance.wtViewport.columnsRenderCalculator, + stretchedWidth; + + if (calculator) { + stretchedWidth = calculator.getStretchedColumnWidth(sourceColumn, width); + + if (stretchedWidth) { + width = stretchedWidth; + } + } + + return width; +}; + + +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'); + this.columnHeaderCount = this.columnHeaders.length; + + var visibleColIndex + , totalRows = this.instance.getSetting('totalRows') + , totalColumns = this.instance.getSetting('totalColumns') + , displayTds + , adjusted = false + , workspaceWidth + , cloneLimit = this.wtTable.getRenderedRowsCount(); + + if (totalColumns > 0) { + this.adjustAvailableNodes(); + adjusted = true; + + this.renderColGroups(); + + this.renderColumnHeaders(); + + displayTds = this.wtTable.getRenderedColumnsCount(); + + //Render table rows + this.renderRows(totalRows, cloneLimit, displayTds); + + if (!this.wtTable.isWorkingOnClone()) { + workspaceWidth = this.instance.wtViewport.getWorkspaceWidth(); + this.instance.wtViewport.containerWidth = null; + } else { + this.adjustColumnHeaderHeights(); + } + + this.adjustColumnWidths(displayTds); + } + + if (!adjusted) { + this.adjustAvailableNodes(); + } + + this.removeRedundantRows(cloneLimit); + + if (!this.wtTable.isWorkingOnClone()) { + this.markOversizedRows(); + + this.instance.wtViewport.createVisibleCalculators(); + + 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; + + var firstRendered = this.wtTable.getFirstRenderedColumn(); + var lastRendered = this.wtTable.getLastRenderedColumn(); + + for (var i = firstRendered ; i < lastRendered; i++) { + var width = this.wtTable.getStretchedColumnWidth(i); + var renderedIndex = this.columnFilter.sourceToRendered(i); + this.COLGROUP.childNodes[renderedIndex + this.rowHeaderCount].style.width = width + 'px'; + } + } + + this.instance.wtScrollbars.refresh(false); + + this.instance.getSetting('onDraw', true); + } + +}; + +WalkontableTableRenderer.prototype.removeRedundantRows = function (renderedRowsCount) { + while (this.wtTable.tbodyChildrenLength > renderedRowsCount) { + this.TBODY.removeChild(this.TBODY.lastChild); + this.wtTable.tbodyChildrenLength--; + } +}; + +WalkontableTableRenderer.prototype.renderRows = function (totalRows, cloneLimit, displayTds) { + var lastTD, TR; + var visibleRowIndex = 0; + var sourceRowIndex = this.rowFilter.renderedToSource(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); + + //after last column is rendered, check if last cell is fully displayed + if (!isWorkingOnClone) { + this.resetOversizedRow(sourceRowIndex); + } + + + if (TR.firstChild) { + //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 + var height = this.instance.wtTable.getRowHeight(sourceRowIndex); + if (height) { + TR.firstChild.style.height = height + 'px'; + } + else { + TR.firstChild.style.height = ''; + } + } + + visibleRowIndex++; + + sourceRowIndex = this.rowFilter.renderedToSource(visibleRowIndex); + } +}; + +WalkontableTableRenderer.prototype.resetOversizedRow = function (sourceRow) { + if (this.instance.wtViewport.oversizedRows && this.instance.wtViewport.oversizedRows[sourceRow]) { + this.instance.wtViewport.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.renderedToSource(rowCount); + previousRowHeight = this.instance.wtTable.getRowHeight(sourceRowIndex); + currentTr = this.instance.wtTable.getTrForRow(sourceRowIndex); + + trInnerHeight = Handsontable.Dom.innerHeight(currentTr) - 1; + + if ((!previousRowHeight && this.instance.wtSettings.settings.defaultRowHeight < trInnerHeight || previousRowHeight < trInnerHeight)) { + this.instance.wtViewport.oversizedRows[sourceRowIndex] = trInnerHeight; + } + } + +}; + +WalkontableTableRenderer.prototype.adjustColumnHeaderHeights = function () { + var columnHeaders = this.instance.getSetting('columnHeaders'); + for(var i = 0, columnHeadersCount = columnHeaders.length; i < columnHeadersCount; i++) { + if(this.instance.wtViewport.oversizedColumnHeaders[i]) { + if(this.instance.wtTable.THEAD.childNodes[i].childNodes.length === 0) { + return; + } + this.instance.wtTable.THEAD.childNodes[i].childNodes[0].style.height = this.instance.wtViewport.oversizedColumnHeaders[i] + "px"; + } + } +}; + +WalkontableTableRenderer.prototype.markIfOversizedColumnHeader = function (col) { + var colCount = this.instance.wtTable.THEAD.childNodes.length !== 0 ? this.instance.wtTable.THEAD.childNodes[0].childNodes.length : 0, + sourceColIndex, + previousColHeaderHeight, + currentHeader, + currentHeaderHeight, + columnHeaders = this.instance.getSetting('columnHeaders'), + columnHeaderCount = columnHeaders.length, + level = columnHeaderCount; + + sourceColIndex = this.instance.wtTable.columnFilter.renderedToSource(col); + + while(level) { + level--; + + previousColHeaderHeight = this.instance.wtTable.getColumnHeaderHeight(level); + currentHeader = this.instance.wtTable.getColumnHeader(sourceColIndex, level); + + if(!currentHeader) { + continue; + } + + currentHeaderHeight = Handsontable.Dom.innerHeight(currentHeader) - 1; + + if ((!previousColHeaderHeight && this.instance.wtSettings.settings.defaultRowHeight < currentHeaderHeight || previousColHeaderHeight < currentHeaderHeight)) { + this.instance.wtViewport.oversizedColumnHeaders[level] = currentHeaderHeight; + } + } +}; + +WalkontableTableRenderer.prototype.renderCells = function (sourceRowIndex, TR, displayTds) { + var TD, sourceColIndex; + + for (var visibleColIndex = 0; visibleColIndex < displayTds; visibleColIndex++) { + sourceColIndex = this.columnFilter.renderedToSource(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); + } + + if (!Handsontable.Dom.hasClass(TD, 'hide')) { + TD.className = ''; + } + + TD.removeAttribute('style'); + + this.instance.wtSettings.settings.cellRenderer(sourceRowIndex, sourceColIndex, TD); + + } + + return TD; +}; + +WalkontableTableRenderer.prototype.adjustColumnWidths = function (displayTds) { + var width; + + this.instance.wtViewport.columnsRenderCalculator.refreshStretching(this.instance.wtViewport.getViewportWidth()); + + for (var renderedColIndex = 0; renderedColIndex < displayTds; renderedColIndex++) { + width = this.wtTable.getStretchedColumnWidth(this.columnFilter.renderedToSource(renderedColIndex)); + this.COLGROUP.childNodes[renderedColIndex + this.rowHeaderCount].style.width = width + 'px'; + } +}; + +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){ + TH.className = ''; + TH.removeAttribute('style'); + this.rowHeaders[col](row, TH, col); +}; + +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 () { + //adjust COLGROUP + this.adjustColGroups(); + + //adjust THEAD + this.adjustThead(); +}; + +WalkontableTableRenderer.prototype.renderColumnHeaders = function () { + if (!this.columnHeaderCount) { + return; + } + + var columnCount = this.wtTable.getRenderedColumnsCount(), + TR, + renderedColumnIndex; + + for (var i = 0; i < this.columnHeaderCount; i++) { + TR = this.getTrForColumnHeaders(i); + + for (renderedColumnIndex = (-1) * this.rowHeaderCount; renderedColumnIndex < columnCount; renderedColumnIndex++) { + var sourceCol = this.columnFilter.renderedToSource(renderedColumnIndex); + this.renderColumnHeader(i, sourceCol, TR.childNodes[renderedColumnIndex + this.rowHeaderCount]); + + if(!this.wtTable.isWorkingOnClone()) { + this.markIfOversizedColumnHeader(renderedColumnIndex); + } + } + } +}; + +WalkontableTableRenderer.prototype.adjustColGroups = function () { + var columnCount = this.wtTable.getRenderedColumnsCount(); + + //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--; + } +}; + +WalkontableTableRenderer.prototype.adjustThead = function () { + var columnCount = this.wtTable.getRenderedColumnsCount(); + var TR = this.THEAD.firstChild; + if (this.columnHeaders.length) { + + for (var i = 0, columnHeadersLength = this.columnHeaders.length; i < columnHeadersLength; i++) { + TR = this.THEAD.childNodes[i]; + 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--; + } + } + + var theadChildrenLength = this.THEAD.childNodes.length; + if(theadChildrenLength > this.columnHeaders.length) { + for(var i = this.columnHeaders.length; i < theadChildrenLength; i++ ) { + this.THEAD.removeChild(this.THEAD.lastChild); + } + } + } + + else if (TR) { + Handsontable.Dom.empty(TR); + } +}; + +WalkontableTableRenderer.prototype.getTrForColumnHeaders = function (index) { + var TR = this.THEAD.childNodes[index]; + return TR; +}; + +WalkontableTableRenderer.prototype.renderColumnHeader = function (row, col, TH) { + TH.className = ''; + TH.removeAttribute('style'); + return this.columnHeaders[row](col, TH, row); +}; + +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.removeRedundantColumns = function (renderedColumnsCount) { + while (this.wtTable.tbodyChildrenLength > renderedColumnsCount) { + this.TBODY.removeChild(this.TBODY.lastChild); + this.wtTable.tbodyChildrenLength--; + } +}; + +/* + 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.oversizedRows = []; + this.oversizedCols = []; + this.oversizedColumnHeaders = []; + + var that = this; + + var eventManager = Handsontable.eventManager(instance); + eventManager.addEventListener(window,'resize',function () { + that.clientHeight = that.getWorkspaceHeight(); + }); +} + +WalkontableViewport.prototype.getWorkspaceHeight = function () { + var scrollHandler = this.instance.wtScrollbars.vertical.scrollHandler; + if (scrollHandler === window) { + return document.documentElement.clientHeight; + } + else { + var elemHeight = Handsontable.Dom.outerHeight(scrollHandler); + var height = (elemHeight > 0 && scrollHandler.clientHeight > 0) ? scrollHandler.clientHeight : Infinity; //returns height without DIV scrollbar + return height; + } +}; + + +WalkontableViewport.prototype.getWorkspaceWidth = function () { + var width, + totalColumns = this.instance.getSetting("totalColumns"), + scrollHandler = this.instance.wtScrollbars.horizontal.scrollHandler, + overflow, + stretchSetting = this.instance.getSetting('stretchH'); + + if(Handsontable.freezeOverlays) { + width = Math.min(document.documentElement.offsetWidth - this.getWorkspaceOffset().left, document.documentElement.offsetWidth); + } else { + width = Math.min(this.getContainerFillWidth(), document.documentElement.offsetWidth - this.getWorkspaceOffset().left, document.documentElement.offsetWidth); + } + + if (scrollHandler === window && totalColumns > 0 && this.sumColumnWidths(0, totalColumns - 1) > width) { + //in case sum of column widths is higher than available stylesheet width, let's assume using the whole window + //otherwise continue below, which will allow stretching + //this is used in `scroll_window.html` + //TODO test me + return document.documentElement.clientWidth; + } + + if (scrollHandler !== window){ + overflow = this.instance.wtScrollbars.horizontal.scrollHandler.style.overflow; + + if (overflow == "scroll" || overflow == "hidden" || overflow == "auto") { + //this is used in `scroll.html` + //TODO test me + return Math.max(width, scrollHandler.clientWidth); + } + } + + if(stretchSetting === 'none' || !stretchSetting) { + // if no stretching is used, return the maximum used workspace width + return Math.max(width, Handsontable.Dom.outerWidth(this.instance.wtTable.TABLE)); + } else { + // if stretching is used, return the actual container width, so the columns can fit inside it + return width; + } +}; + +WalkontableViewport.prototype.sumColumnWidths = function (from, length) { + var sum = 0; + while(from < length) { + sum += this.instance.wtTable.getColumnWidth(from) || this.instance.wtSettings.defaultColumnWidth; + from++; + } + return sum; +}; +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 offsetWidth; +}; + +WalkontableViewport.prototype.getColumnHeaderHeight = function () { + if (isNaN(this.columnHeaderHeight)) { + this.columnHeaderHeight = Handsontable.Dom.outerHeight(this.instance.wtTable.THEAD); + } + return this.columnHeaderHeight; +}; + +WalkontableViewport.prototype.getViewportHeight = function () { + + var containerHeight = this.getWorkspaceHeight(); + + 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 () { + var containerWidth = this.getWorkspaceWidth(), + rowHeaderWidth; + + if (containerWidth === Infinity) { + return containerWidth; + } + rowHeaderWidth = this.getRowHeaderWidth(); + + if (rowHeaderWidth > 0) { + return containerWidth - rowHeaderWidth; + } + + return containerWidth; +}; + +/** + * Creates: + * - rowsRenderCalculator (before draw, to qualify rows for rendering) + * - rowsVisibleCalculator (after draw, to measure which rows are actually visible) + * @returns {WalkontableViewportRowsCalculator} + */ +WalkontableViewport.prototype.createRowsCalculator = function (visible) { + this.rowHeaderWidth = NaN; + + var height; + if (this.instance.wtSettings.settings.renderAllRows) { + height = Infinity; + } + else { + height = this.getViewportHeight(); + } + + var pos = this.instance.wtScrollbars.vertical.getScrollPosition() - this.instance.wtScrollbars.vertical.getTableParentOffset(); + if (pos < 0) { + pos = 0; + } + + var fixedRowsTop = this.instance.getSetting('fixedRowsTop'); + if(fixedRowsTop) { + var fixedRowsHeight = this.instance.wtScrollbars.vertical.sumCellSizes(0, fixedRowsTop); + pos += fixedRowsHeight; + height -= fixedRowsHeight; + } + + var that = this; + return new WalkontableViewportRowsCalculator( + height, + pos, + this.instance.getSetting('totalRows'), + function(sourceRow) { + return that.instance.wtTable.getRowHeight(sourceRow); + }, + visible ? null : this.instance.wtSettings.settings.viewportRowCalculatorOverride, + visible ? true : false + ); +}; + +/** + * Creates: + * - columnsRenderCalculator (before draw, to qualify columns for rendering) + * - columnsVisibleCalculator (after draw, to measure which columns are actually visible) + * @returns {WalkontableViewportRowsCalculator} + */ +WalkontableViewport.prototype.createColumnsCalculator = function (visible) { + this.columnHeaderHeight = NaN; + + var width = this.getViewportWidth(); + + var pos = this.instance.wtScrollbars.horizontal.getScrollPosition() - this.instance.wtScrollbars.vertical.getTableParentOffset(); + if (pos < 0) { + pos = 0; + } + + var fixedColumnsLeft = this.instance.getSetting('fixedColumnsLeft'); + if(fixedColumnsLeft) { + var fixedColumnsWidth = this.instance.wtScrollbars.horizontal.sumCellSizes(0, fixedColumnsLeft); + pos += fixedColumnsWidth; + width -= fixedColumnsWidth; + } + + var that = this; + return new WalkontableViewportColumnsCalculator( + width, + pos, + this.instance.getSetting('totalColumns'), + function (sourceCol) { + return that.instance.wtTable.getColumnWidth(sourceCol); + }, + visible ? null : this.instance.wtSettings.settings.viewportColumnCalculatorOverride, + visible ? true : false, + this.instance.getSetting('stretchH') + ); +}; + + +/** + * Creates rowsRenderCalculator and columnsRenderCalculator (before draw, to determine what rows and cols should be rendered) + * @param fastDraw {Boolean} If TRUE, will try to avoid full redraw and only update the border positions. If FALSE or UNDEFINED, will perform a full redraw + */ +WalkontableViewport.prototype.createRenderCalculators = function (fastDraw) { + if (fastDraw) { + var proposedRowsVisibleCalculator = this.createRowsCalculator(true); + var proposedColumnsVisibleCalculator = this.createColumnsCalculator(true); + if (!(this.areAllProposedVisibleRowsAlreadyRendered(proposedRowsVisibleCalculator) && this.areAllProposedVisibleColumnsAlreadyRendered(proposedColumnsVisibleCalculator) ) ) { + fastDraw = false; + } + } + + if(!fastDraw) { + this.rowsRenderCalculator = this.createRowsCalculator(); + this.columnsRenderCalculator = this.createColumnsCalculator(); + } + + this.rowsVisibleCalculator = null; //delete temporarily to make sure that renderers always use rowsRenderCalculator, not rowsVisibleCalculator + this.columnsVisibleCalculator = null; + + return fastDraw; +}; + +/** + * Creates rowsVisibleCalculator and columnsVisibleCalculator (after draw, to determine what are the actually visible rows and columns) + */ +WalkontableViewport.prototype.createVisibleCalculators = function () { + this.rowsVisibleCalculator = this.createRowsCalculator(true); + this.columnsVisibleCalculator = this.createColumnsCalculator(true); +}; + +/** + * Returns information whether proposedRowsVisibleCalculator viewport + * is contained inside rows rendered in previous draw (cached in rowsRenderCalculator) + * + * Returns TRUE if all proposed visible rows are already rendered (meaning: redraw is not needed) + * Returns FALSE if at least one proposed visible row is not already rendered (meaning: redraw is needed) + * + * @returns {boolean} + */ +WalkontableViewport.prototype.areAllProposedVisibleRowsAlreadyRendered = function (proposedRowsVisibleCalculator) { + if (this.rowsVisibleCalculator) { + if (proposedRowsVisibleCalculator.startRow < this.rowsRenderCalculator.startRow || + (proposedRowsVisibleCalculator.startRow === this.rowsRenderCalculator.startRow && + proposedRowsVisibleCalculator.startRow > 0)) { + return false; + } + else if (proposedRowsVisibleCalculator.endRow > this.rowsRenderCalculator.endRow || + (proposedRowsVisibleCalculator.endRow === this.rowsRenderCalculator.endRow && + proposedRowsVisibleCalculator.endRow < this.instance.getSetting('totalRows') - 1)) { + return false; + } + else { + return true; + } + } + return false; +}; + +/** + * Returns information whether proposedColumnsVisibleCalculator viewport + * is contained inside column rendered in previous draw (cached in columnsRenderCalculator) + * + * Returns TRUE if all proposed visible columns are already rendered (meaning: redraw is not needed) + * Returns FALSE if at least one proposed visible column is not already rendered (meaning: redraw is needed) + * + * @returns {boolean} + */ +WalkontableViewport.prototype.areAllProposedVisibleColumnsAlreadyRendered = function (proposedColumnsVisibleCalculator) { + if (this.columnsVisibleCalculator) { + if (proposedColumnsVisibleCalculator.startColumn < this.columnsRenderCalculator.startColumn || + (proposedColumnsVisibleCalculator.startColumn === this.columnsRenderCalculator.startColumn && + proposedColumnsVisibleCalculator.startColumn > 0)) { + return false; + } + else if (proposedColumnsVisibleCalculator.endColumn > this.columnsRenderCalculator.endColumn || + (proposedColumnsVisibleCalculator.endColumn === this.columnsRenderCalculator.endColumn && + proposedColumnsVisibleCalculator.endColumn < this.instance.getSetting('totalColumns') - 1)) { + return false; + } + else { + return true; + } + } + return false; +}; + +function WalkontableViewportColumnsCalculator (width, scrollOffset, totalColumns, columnWidthFn, overrideFn, onlyFullyVisible, stretchH) { + var + _this = this, + ratio = 1, + sum = 0, + needReverse = true, + defaultColumnWidth = 50, + startPositions = [], + getColumnWidth, + columnWidth, i; + + this.scrollOffset = scrollOffset; + this.startColumn = null; + this.endColumn = null; + this.startPosition = null; + this.count = 0; + this.stretchAllRatio = 0; + this.stretchLastWidth = 0; + this.stretch = stretchH; + this.totalTargetWidth = 0; + this.needVerifyLastColumnWidth = true; + this.stretchAllColumnsWidth = []; + + + function getStretchedAllColumnWidth(column, baseWidth) { + var sumRatioWidth = 0; + + if (!_this.stretchAllColumnsWidth[column]) { + _this.stretchAllColumnsWidth[column] = Math.round(baseWidth * _this.stretchAllRatio); + } + if (_this.stretchAllColumnsWidth.length === totalColumns && _this.needVerifyLastColumnWidth) { + _this.needVerifyLastColumnWidth = false; + + for (var i = 0; i < _this.stretchAllColumnsWidth.length; i++) { + sumRatioWidth += _this.stretchAllColumnsWidth[i]; + } + if (sumRatioWidth != _this.totalTargetWidth) { + _this.stretchAllColumnsWidth[_this.stretchAllColumnsWidth.length - 1] += _this.totalTargetWidth - sumRatioWidth; + } + } + + return _this.stretchAllColumnsWidth[column]; + } + + function getStretchedLastColumnWidth(column, baseWidth) { + if (column === totalColumns - 1) { + return _this.stretchLastWidth; + } + + return null; + } + + getColumnWidth = function getColumnWidth(i) { + var width = columnWidthFn(i); + + ratio = ratio || 1; + + if (width === undefined) { + width = defaultColumnWidth; + } + + return width; + }; + + /** + * Recalculate columns stretching. + * + * @param {Number} totalWidth + */ + this.refreshStretching = function (totalWidth) { + var sumAll = 0, + columnWidth, + remainingSize; + + for (var i = 0; i < totalColumns; i++) { + columnWidth = getColumnWidth(i); + sumAll += columnWidth; + } + this.totalTargetWidth = totalWidth; + remainingSize = sumAll - totalWidth; + + if (this.stretch === 'all' && remainingSize < 0) { + this.stretchAllRatio = totalWidth / sumAll; + this.stretchAllColumnsWidth = []; + this.needVerifyLastColumnWidth = true; + } + else if (this.stretch === 'last' && totalWidth !== Infinity) { + this.stretchLastWidth = -remainingSize + getColumnWidth(totalColumns - 1); + } + }; + + /** + * Get stretched column width based on stretchH (all or last) setting passed in handsontable instance. + * + * @param {Number} column + * @param {Number} baseWidth + * @returns {Number|null} + */ + this.getStretchedColumnWidth = function(column, baseWidth) { + var result = null; + + if (this.stretch === 'all' && this.stretchAllRatio !== 0) { + result = getStretchedAllColumnWidth(column, baseWidth); + } + else if (this.stretch === 'last' && this.stretchLastWidth !== 0) { + result = getStretchedLastColumnWidth(column, baseWidth); + } + + return result; + }; + + + for (i = 0; i < totalColumns; i++) { + columnWidth = getColumnWidth(i); + + if (sum <= scrollOffset && !onlyFullyVisible) { + this.startColumn = i; + } + + if (sum >= scrollOffset && sum + columnWidth <= scrollOffset + width) { + if (this.startColumn == null) { + this.startColumn = i; + } + this.endColumn = i; + } + startPositions.push(sum); + sum += columnWidth; + + if (!onlyFullyVisible) { + this.endColumn = i; + } + if (sum >= scrollOffset + width) { + needReverse = false; + break; + } + } + + if (this.endColumn == totalColumns - 1 && needReverse) { + this.startColumn = this.endColumn; + + while (this.startColumn > 0) { + var viewportSum = startPositions[this.endColumn] + columnWidth - startPositions[this.startColumn - 1]; + + if (viewportSum <= width || !onlyFullyVisible) { + this.startColumn--; + } + if (viewportSum > width) { + break; + } + } + } + + if (this.startColumn !== null && overrideFn) { + overrideFn(this); + } + this.startPosition = startPositions[this.startColumn]; + + if (this.startPosition == void 0) { + this.startPosition = null; + } + if (this.startColumn != null) { + this.count = this.endColumn - this.startColumn + 1; + } +} + + +/** + * Viewport calculator constructor. Calculates indexes of rows to render OR rows that are visible. + * To redo the calculation, you need to create a new calculator. + * + * Object properties: + * this.scrollOffset - position of vertical scroll (in px) + * this.startRow - index of the first rendered/visible row (can be overwritten using overrideFn) + * this.startPosition - position of the first rendered/visible row (in px) + * this.endRow - index of the last rendered/visible row (can be overwritten using overrideFn) + * this.count - number of rendered/visible rows + * + * @param height - height of the viewport + * @param scrollOffset - current vertical scroll position of the viewport + * @param totalRows - total number of rows + * @param rowHeightFn - function that returns the height of the row at a given index (in px) + * @param overrideFn - function that changes calculated this.startRow, this.endRow (used by mergeCells.js plugin) + * @param onlyFullyVisible {bool} - if TRUE, only startRow and endRow will be indexes of rows that are FULLY in viewport + * @constructor + */ +function WalkontableViewportRowsCalculator(height, scrollOffset, totalRows, rowHeightFn, overrideFn, onlyFullyVisible) { + this.scrollOffset = scrollOffset; + this.startRow = null; + this.startPosition = null; + this.endRow = null; + this.count = 0; + var sum = 0; + var rowHeight; + var needReverse = true; + var defaultRowHeight = 23; + var startPositions = []; + for (var i = 0; i < totalRows; i++) { + rowHeight = rowHeightFn(i); + if (rowHeight === undefined) { + rowHeight = defaultRowHeight; + } + if (sum <= scrollOffset && !onlyFullyVisible) { + this.startRow = i; + } + if (sum >= scrollOffset && sum + rowHeight <= scrollOffset + height) { + if (this.startRow == null) { + this.startRow = i; + } + this.endRow = i; + } + startPositions.push(sum); + sum += rowHeight; + if(!onlyFullyVisible) { + this.endRow = i; + } + if (sum >= scrollOffset + height) { + needReverse = false; + break; + } + } + + //If the rendering has reached the last row and there is still some space available in the viewport, we need to render in reverse in order to fill the whole viewport with rows + if (this.endRow == totalRows - 1 && needReverse) { + this.startRow = this.endRow; + while(this.startRow > 0) { + var viewportSum = startPositions[this.endRow] + rowHeight - startPositions[this.startRow - 1]; //rowHeight is the height of the last row + if (viewportSum <= height || !onlyFullyVisible) + { + this.startRow--; + } + if (viewportSum >= height) + { + break; + } + } + } + + if (this.startRow !== null && overrideFn) { + overrideFn(this); + } + + this.startPosition = startPositions[this.startRow]; + if (this.startPosition == void 0) { + this.startPosition = null; + } + + if (this.startRow != null) { + this.count = this.endRow - this.startRow + 1; + } +} + +if (window.jQuery) { + (function (window, $, Handsontable) { + $.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[0], 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); + + if (action === 'destroy'){ + $this.removeData(); + } + } + else { + throw new Error('Handsontable do not provide action: ' + action); + } + } + + return output; + } + }; + })(window, jQuery, Handsontable); +} + + + +})(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; + } + + key = key.toLowerCase(); + + 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'; + }; + + numeral.validate = function(val, culture) { + + var _decimalSep, + _thousandSep, + _currSymbol, + _valArray, + _abbrObj, + _thousandRegEx, + languageData, + temp; + + //coerce val to string + if (typeof val !== 'string') { + val += ''; + if (console.warn) { + console.warn('Numeral.js: Value is not string. It has been co-erced to: ', val); + } + } + + //trim whitespaces from either sides + val = val.trim(); + + + //if val is empty return false + if (val === '') { + return false; + } + + //replace the initial '+' or '-' sign if present + val = val.replace(/^[+-]?/, ''); + + + //get the decimal and thousands separator from numeral.languageData + try { + //check if the culture is understood by numeral. if not, default it to current language + languageData = numeral.languageData(culture); + } catch (e) { + languageData = numeral.languageData(numeral.language()); + } + + //setup the delimiters and currency symbol based on culture/language + _currSymbol = languageData.currency.symbol; + _abbrObj = languageData.abbreviations; + _decimalSep = languageData.delimiters.decimal; + if (languageData.delimiters.thousands === '.') { + _thousandSep = '\\.'; + } else { + _thousandSep = languageData.delimiters.thousands; + } + + //validating currency symbol + temp = val.match(/^[^\d]+/); + if (temp !== null) { + //chuck the currency symbol away + val = val.substr(1); + if (temp[0] !== _currSymbol) { + return false; + } + } + + //validating abbreviation symbol + temp = val.match(/[^\d]+$/); + if (temp !== null) { + val = val.slice(0, - 1); + if (temp[0] !== _abbrObj.thousand && temp[0] !== _abbrObj.million && temp[0] !== _abbrObj.billion && temp[0] !== _abbrObj.trillion) { + return false; + } + } + + //if val is just digits the return true + if ( !! val.match(/^\d+$/)) { + return true; + } + _thousandRegEx = new RegExp(_thousandSep + '{2}'); + + if (!val.match(/[^\d.,]/g)) { + _valArray = val.split(_decimalSep); + if (_valArray.length > 2) { + return false; + } else { + if (_valArray.length < 2) { + return ( !! _valArray[0].match(/^\d+.*\d$/) && !_valArray[0].match(_thousandRegEx)); + } else { + if (_valArray[0].length === 1) { + return ( !! _valArray[0].match(/^\d+$/) && !_valArray[0].match(_thousandRegEx) && !! _valArray[1].match(/^\d+$/)); + } else { + return ( !! _valArray[0].match(/^\d+.*\d$/) && !_valArray[0].match(_thousandRegEx) && !! _valArray[1].match(/^\d+$/)); + } + } + } + } + + return false; + }; + + /************************************ + 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/handsontable.full.min.css b/bower_components/handsontable/dist/handsontable.full.min.css new file mode 100644 index 0000000..9912658 --- /dev/null +++ b/bower_components/handsontable/dist/handsontable.full.min.css @@ -0,0 +1,10 @@ +/*! + * Handsontable 0.12.4 + * Handsontable is a JavaScript library 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: Fri Jan 23 2015 10:07:24 GMT+0100 (CET) + */.handsontable{position:relative}.handsontable .hide{display:none}.handsontable .relative{position:relative}.handsontable.htAutoColumnSize{visibility:hidden;left:0;position:absolute;top:0}.handsontable .wtHider{width:0}.handsontable .wtSpreader{position:relative;width:0;height:auto}.handsontable div,.handsontable input,.handsontable table,.handsontable tbody,.handsontable td,.handsontable textarea,.handsontable th,.handsontable thead{box-sizing:content-box;-webkit-box-sizing:content-box;-moz-box-sizing:content-box}.handsontable input,.handsontable textarea{min-height:initial}.handsontable table.htCore{border-collapse:separate;border-spacing:0;margin:0;border-width:0;table-layout:fixed;width:0;outline-width:0;max-width:none;max-height:none}.handsontable col,.handsontable col.rowHeader{width:50px}.handsontable td,.handsontable th{border-right:1px solid #CCC;border-bottom:1px solid #CCC;height:22px;empty-cells:show;line-height:21px;padding:0 4px;background-color:#FFF;vertical-align:top;overflow:hidden;outline-width:0;white-space:pre-line}.handsontable td.htInvalid{background-color:#ff4c42!important}.handsontable td.htNoWrap{white-space:nowrap}.handsontable th:last-child{border-right:1px solid #CCC;border-bottom:1px solid #CCC}.handsontable th.htNoFrame,.handsontable th:first-child.htNoFrame,.handsontable tr:first-child th.htNoFrame{border-left-width:0;background-color:#fff;border-color:#FFF}.handsontable .htNoFrame+td,.handsontable .htNoFrame+th,.handsontable td:first-child,.handsontable th:first-child{border-left:1px solid #CCC}.handsontable tr:first-child td,.handsontable tr:first-child th{border-top:1px solid #CCC}.handsontable thead tr.lastChild th,.handsontable thead tr:last-child th{border-bottom-width:0}.handsontable th{background-color:#EEE;color:#222;text-align:center;font-weight:400;white-space:nowrap}.handsontable thead th{padding:0}.handsontable th.active{background-color:#CCC}.handsontable thead th .relative{padding:2px 4px}.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:.7;display:none}.handsontable .manualColumnMoverGuide.active,.handsontable .manualRowMoverGuide.active{display:block}.handsontable .manualColumnMover.active,.handsontable .manualColumnMover:hover,.handsontable .manualRowMover.active,.handsontable .manualRowMover:hover{background-color:#88F}.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.active,.handsontable .manualColumnResizer:hover,.handsontable .manualRowResizer.active,.handsontable .manualRowResizer:hover{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}.handsontable .wtBorder{position:absolute;font-size:0}.handsontable .wtBorder.hidden{display:none!important}.handsontable td.area{background-color:#EEF4FF}.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:inherit;line-height:inherit;font-size:inherit;-webkit-box-shadow:1px 2px 5px rgba(0,0,0,.4);box-shadow:1px 2px 5px rgba(0,0,0,.4);resize:none;display:inline-block;color:#000;border-radius:0;background-color:#FFF}.handsontableInput:focus{border:2px solid #5292F7}.handsontableInputHolder{position:absolute;top:0;left:0;z-index:100}.htSelectEditor{-webkit-appearance:menulist-button!important;position:absolute;width:auto}.handsontable .htDimmed{color:#777}.handsontable .htSubmenu{position:relative}.handsontable .htSubmenu :after{content:'▶';color:#777;position:absolute;right:5px}.handsontable .htLeft{text-align:left}.handsontable .htCenter{text-align:center}.handsontable .htRight{text-align:right}.handsontable .htJustify{text-align:justify}.handsontable .htTop{vertical-align:top}.handsontable .htMiddle{vertical-align:middle}.handsontable .htBottom{vertical-align:bottom}.handsontable .htPlaceholder{color:#999}.handsontable .htAutocompleteArrow{float:right;font-size:10px;color:#EEE;cursor:default;width:16px;text-align:center}.handsontable td .htAutocompleteArrow:hover{color:#777}.handsontable .htCheckboxRendererInput.noValue{opacity:.5}.handsontable .htNumeric{text-align:right}.htCommentCell{position:relative}.htCommentCell:after{content:'';position:absolute;top:0;right:0;border-left:6px solid transparent;border-top:6px solid red}@-webkit-keyframes opacity-hide{from{opacity:1}to{opacity:0}}@keyframes opacity-hide{from{opacity:1}to{opacity:0}}@-webkit-keyframes opacity-show{from{opacity:0}to{opacity:1}}@keyframes opacity-show{from{opacity:0}to{opacity:1}}.handsontable .handsontable .wtHider{padding:0 0 5px}.handsontable .handsontable table{-webkit-box-shadow:1px 2px 5px rgba(0,0,0,.4);box-shadow:1px 2px 5px rgba(0,0,0,.4)}.handsontable .autocompleteEditor.handsontable{padding-right:17px}.handsontable .autocompleteEditor.handsontable.htMacScroll{padding-right:15px}.handsontable.listbox{margin:0}.handsontable.listbox .ht_master table{border:1px solid #ccc;border-collapse:separate;background:#fff}.handsontable.listbox td,.handsontable.listbox th,.handsontable.listbox tr:first-child td,.handsontable.listbox tr:first-child th,.handsontable.listbox tr:last-child th{border-width:0}.handsontable.listbox td,.handsontable.listbox th{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:1060}.htContextMenu .ht_clone_corner,.htContextMenu .ht_clone_debug,.htContextMenu .ht_clone_left,.htContextMenu .ht_clone_top{display:none}.ht_clone_top{z-index:101}.ht_clone_left{z-index:102}.ht_clone_corner,.ht_clone_debug{z-index:103}.htContextMenu table.htCore{outline:#bbb solid 1px}.htContextMenu .wtBorder{visibility:hidden}.htContextMenu table tbody tr td{background:#fff;border-width:0;padding:4px 6px 0;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,.htContextMenu table tbody tr td.zeroclipboard-is-hover{background:#e9e9e9}.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:#fff;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}.htBordered{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}.handsontable colgroup col.rowHeader.htGroupCol{width:25px!important}.handsontable colgroup col.rowHeader.htGroupColClosest{width:30px!important}.handsontable .htGroupIndicatorContainer{background:#fff;border:0;padding-bottom:0;vertical-align:bottom;position:relative}.handsontable thead .htGroupIndicatorContainer{vertical-align:top;border-bottom:0}.handsontable tbody tr th:nth-last-child(2){border-right:1px solid #CCC}.handsontable thead tr:nth-last-child(2) th{border-bottom:1px solid #CCC;padding-bottom:5px}.ht_clone_corner thead tr th:nth-last-child(2){border-right:1px solid #CCC}.htVerticalGroup{height:100%}.htHorizontalGroup{width:100%;height:100%}.htVerticalGroup:not(.htCollapseButton):after{content:"";height:100%;width:1px;display:block;background:#ccc;margin-left:5px}.htHorizontalGroup:not(.htCollapseButton):after{content:"";width:100%;height:1px;display:block;background:#ccc;margin-top:20%}.htCollapseButton{width:10px;height:10px;line-height:10px;text-align:center;border-radius:5px;border:1px solid #f3f3f3;-webkit-box-shadow:1px 1px 3px rgba(0,0,0,.4);box-shadow:1px 1px 3px rgba(0,0,0,.4);cursor:pointer;margin-bottom:3px;position:relative}.htCollapseButton:after{content:"";height:300%;width:1px;display:block;background:#ccc;margin-left:4px;position:absolute;bottom:10px}thead .htCollapseButton{right:5px;position:absolute;top:5px;background:#fff}thead .htCollapseButton:after{height:1px;width:700%;right:10px;top:4px}.handsontable tr th .htGroupStart:after{background:0 0;border-left:1px solid #ccc;border-top:1px solid #ccc;width:5px;position:relative;top:50%}.handsontable thead tr th .htGroupStart:after{background:0 0;border-left:1px solid #ccc;border-top:1px solid #ccc;height:5px;width:50%;position:relative;top:0;left:50%}.handsontable .htGroupLevelTrigger{-webkit-box-shadow:1px 1px 3px rgba(0,0,0,.4);box-shadow:1px 1px 3px rgba(0,0,0,.4);width:15px;height:15px;margin:4px auto;padding:0;line-height:15px;cursor:pointer}.handsontable tr th .htExpandButton{position:absolute;width:10px;height:10px;line-height:10px;text-align:center;border-radius:5px;border:1px solid #f3f3f3;-webkit-box-shadow:1px 1px 3px rgba(0,0,0,.4);box-shadow:1px 1px 3px rgba(0,0,0,.4);cursor:pointer;top:0;display:none}.handsontable thead tr th .htExpandButton{top:5px}.handsontable tr th .htExpandButton.clickable{display:block}.handsontable col.hidden{width:0!important}.handsontable tr.hidden,.handsontable tr.hidden td,.handsontable tr.hidden th,.wtDebugHidden{display:none}.wtDebugVisible{display:block;-webkit-animation-duration:.5s;-webkit-animation-name:wtFadeInFromNone;animation-duration:.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}}.handsontable.mobile{-webkit-touch-callout:none;-webkit-user-select:none;-khtml-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;-webkit-tap-highlight-color:transparent;-webkit-overflow-scrolling:touch}.htMobileEditorContainer{display:none;position:absolute;top:0;width:70%;height:54pt;background:#f8f8f8;border-radius:20px;border:1px solid #ebebeb;z-index:999;box-sizing:border-box;-webkit-box-sizing:border-box;-webkit-text-size-adjust:none}.topLeftSelectionHandle-HitArea:not(.ht_master .topLeftSelectionHandle-HitArea),.topLeftSelectionHandle:not(.ht_master .topLeftSelectionHandle){z-index:9999}.bottomRightSelectionHandle,.bottomRightSelectionHandle-HitArea,.topLeftSelectionHandle,.topLeftSelectionHandle-HitArea{left:-10000px;top:-10000px}.htMobileEditorContainer.active{display:block}.htMobileEditorContainer .inputs{position:absolute;right:210pt;bottom:10pt;top:10pt;left:14px;height:34pt}.htMobileEditorContainer .inputs textarea{font-size:13pt;border:1px solid #a1a1a1;-webkit-appearance:none;-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none;position:absolute;left:14px;right:14px;top:0;bottom:0;padding:7pt}.htMobileEditorContainer .cellPointer{position:absolute;top:-13pt;height:0;width:0;left:30px;border-left:13pt solid transparent;border-right:13pt solid transparent;border-bottom:13pt solid #ebebeb}.htMobileEditorContainer .cellPointer.hidden{display:none}.htMobileEditorContainer .cellPointer:before{content:'';display:block;position:absolute;top:2px;height:0;width:0;left:-13pt;border-left:13pt solid transparent;border-right:13pt solid transparent;border-bottom:13pt solid #f8f8f8}.htMobileEditorContainer .moveHandle{position:absolute;top:10pt;left:5px;width:30px;bottom:0;cursor:move;z-index:9999}.htMobileEditorContainer .moveHandle:after{content:"..\a..\a..\a..";white-space:pre;line-height:10px;font-size:20pt;display:inline-block;margin-top:-8px;color:#ebebeb}.htMobileEditorContainer .positionControls{width:205pt;position:absolute;right:5pt;top:0;bottom:0}.htMobileEditorContainer .positionControls>div{width:50pt;height:100%;float:left}.htMobileEditorContainer .positionControls>div:after{content:" ";display:block;width:15pt;height:15pt;text-align:center;line-height:50pt}.htMobileEditorContainer .downButton:after,.htMobileEditorContainer .leftButton:after,.htMobileEditorContainer .rightButton:after,.htMobileEditorContainer .upButton:after{transform-origin:5pt 5pt;-webkit-transform-origin:5pt 5pt;margin:21pt 0 0 21pt}.htMobileEditorContainer .leftButton:after{border-top:2px solid #288ffe;border-left:2px solid #288ffe;-webkit-transform:rotate(-45deg)}.htMobileEditorContainer .leftButton:active:after{border-color:#cfcfcf}.htMobileEditorContainer .rightButton:after{border-top:2px solid #288ffe;border-left:2px solid #288ffe;-webkit-transform:rotate(135deg)}.htMobileEditorContainer .rightButton:active:after{border-color:#cfcfcf}.htMobileEditorContainer .upButton:after{border-top:2px solid #288ffe;border-left:2px solid #288ffe;-webkit-transform:rotate(45deg)}.htMobileEditorContainer .upButton:active:after{border-color:#cfcfcf}.htMobileEditorContainer .downButton:after{border-top:2px solid #288ffe;border-left:2px solid #288ffe;-webkit-transform:rotate(225deg)}.htMobileEditorContainer .downButton:active:after{border-color:#cfcfcf}.handsontable.hide-tween{-webkit-animation:opacity-hide .3s;animation:opacity-hide .3s;animation-fill-mode:forwards;-webkit-animation-fill-mode:forwards}.handsontable.show-tween{-webkit-animation:opacity-show .3s;animation:opacity-show .3s;animation-fill-mode:forwards;-webkit-animation-fill-mode:forwards} \ No newline at end of file diff --git a/bower_components/handsontable/dist/handsontable.full.min.js b/bower_components/handsontable/dist/handsontable.full.min.js new file mode 100644 index 0000000..bc820e1 --- /dev/null +++ b/bower_components/handsontable/dist/handsontable.full.min.js @@ -0,0 +1,26 @@ +/*! + * Handsontable 0.12.4 + * Handsontable is a JavaScript library 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: Fri Jan 23 2015 10:07:24 GMT+0100 (CET) + */ +var Handsontable=function(a,b){b=b||{};var c=new Handsontable.Core(a,b);return c.init(),c};Handsontable.plugins={},function(a,b){"use strict";function c(){this.refCounter=0,this.init()}function d(){function a(){var a=this;a._registerTimeout(setTimeout(function(){a.updateSettings({observeChanges:!0})},0))}function c(a){return function(b,c){return"string"==typeof b[1]&&(b[1]=b[1].toLowerCase()),"string"==typeof c[1]&&(c[1]=c[1].toLowerCase()),b[1]===c[1]?0:null===b[1]||""===b[1]?1:null===c[1]||""===c[1]?-1:b[1]c[1]?a?1:-1:0}}function d(a){return function(b,c){if(b[1]===c[1])return 0;if(null===b[1])return 1;if(null===c[1])return-1;var d=new Date(b[1]),e=new Date(c[1]);return e>d?a?-1:1:d>e?a?1:-1:0}}function e(a){return"undefined"!=typeof a.sortColumn}var f=this;this.init=function(b){var c,d,e=this,g=e.getSettings().columnSorting;if(e.sortingEnabled=!!g,e.sortingEnabled){e.sortIndex=[];var j=h.call(e);"undefined"!=typeof j?(c=j.sortColumn,d=j.sortOrder):(c=g.column,d=g.sortOrder),f.sortByColumn.call(e,c,d),e.sort=function(){var a=Array.prototype.slice.call(arguments);return f.sortByColumn.apply(e,a)},"undefined"==typeof e.getSettings().observeChanges&&a.call(e),"afterInit"==b&&(i.call(e),e.addHook("afterCreateRow",f.afterCreateRow),e.addHook("afterRemoveRow",f.afterRemoveRow),e.addHook("afterLoadData",f.init))}else delete e.sort,e.removeHook("afterCreateRow",f.afterCreateRow),e.removeHook("afterRemoveRow",f.afterRemoveRow),e.removeHook("afterLoadData",f.init)},this.setSortingColumn=function(a,b){var c=this;return"undefined"==typeof a?(delete c.sortColumn,void delete c.sortOrder):(c.sortOrder=c.sortColumn===a&&"undefined"==typeof b?!c.sortOrder:"undefined"!=typeof b?b:!0,void(c.sortColumn=a))},this.sortByColumn=function(a,c){var d=this;f.setSortingColumn.call(d,a,c),"undefined"!=typeof d.sortColumn&&(b.hooks.run(d,"beforeColumnSort",d.sortColumn,d.sortOrder),f.sort.call(d),d.render(),g.call(d),b.hooks.run(d,"afterColumnSort",d.sortColumn,d.sortOrder))};var g=function(){var a=this,c={};"undefined"!=typeof a.sortColumn&&(c.sortColumn=a.sortColumn),"undefined"!=typeof a.sortOrder&&(c.sortOrder=a.sortOrder),(c.hasOwnProperty("sortColumn")||c.hasOwnProperty("sortOrder"))&&b.hooks.run(a,"persistentStateSave","columnSorting",c)},h=function(){var a=this,c={};return b.hooks.run(a,"persistentStateLoad","columnSorting",c),c.value},i=function(){function a(){var a=d.view.TBODY.querySelector("tr").querySelectorAll("th");return a.length}function c(c){var d=b.Dom.closest(c,"TH");return b.Dom.index(d)-a()}var d=this,e=b.eventManager(d);e.addEventListener(d.rootElement,"click",function(a){if(b.Dom.hasClass(a.target,"columnSorting")){var e=c(a.target);f.sortByColumn.call(d,e)}})};this.sort=function(){var a=this;if("undefined"!=typeof a.sortOrder){a.sortingEnabled=!1,a.sortIndex.length=0;for(var b=this.colOffset(),e=0,f=this.countRows()-a.getSettings().minSpareRows;f>e;e++)this.sortIndex.push([e,a.getDataAtCell(e,this.sortColumn+b)]);var g,h=a.getCellMeta(0,a.sortColumn);switch(h.type){case"date":g=d;break;default:g=c}this.sortIndex.sort(g(a.sortOrder));for(var e=this.sortIndex.length;e=0&&b.Dom.addClass(c.querySelector(".colHeader"),"columnSorting")},this.afterCreateRow=function(a,b){var c=this;if(e(c)){for(var d=0;d=a&&(c.sortIndex[d][0]+=b);for(var d=0;b>d;d++)c.sortIndex.splice(a+d,0,[a+d,c.getData()[a+d][c.sortColumn+c.colOffset()]]);g.call(c)}},this.afterRemoveRow=function(a,b){var c=this;if(e(c)){var d=f.translateRow.call(c,a);c.sortIndex.splice(a,b);for(var h=0;hd&&(c.sortIndex[h][0]-=b);g.call(c)}},this.afterChangeSort=function(a){var b=this,c=!1,d={};if(a){for(var e=0;e1&&e(d,g,a),h(),f()}}c.addEventListener(document,"mousedown",b.helper.proxy(g))},h=function(){c.removeEventListener(document,"mousedown"),c.addEventListener(document,"mousedown",b.helper.proxy(k))},i=function(c,d){var e=a.view.wt.wtTable.getCell(c.from),f=b.Dom.offset(e),h=a.getColWidth(c.from.col);d.style.position="absolute",d.style.left=f.left+h+"px",d.style.top=f.top+"px",d.style.zIndex=2,g(c,d)},j=function(a){var c=document.querySelector(".htComments");if(!c){c=document.createElement("DIV");var d=document.createElement("TEXTAREA");b.Dom.addClass(d,"htCommentTextArea"),c.appendChild(d),b.Dom.addClass(c,"htComments"),document.getElementsByTagName("body")[0].appendChild(c)}return a=a||"",document.querySelector(".htCommentTextArea").value=a,c},k=function(c){if(-1!=c.target.className.indexOf("htCommentCell")){h();var d=a.view.wt.wtTable.getCoords(c.target),e={from:new l(d.row,d.col)};b.Comments.showComment(e)}else"htCommentTextArea"!=c.target.className&&f()};return{init:function(){c.addEventListener(document,"mouseover",b.helper.proxy(k))},showComment:function(b){var c=a.getCellMeta(b.from.row,b.from.col),d="";c.comment&&(d=c.comment);var e=j(d);e.style.display="block",i(b,e)},removeComment:function(b,c){a.removeCellMeta(b,c,"comment"),a.render()},checkSelectionCommentsConsistency:function(){var b=!1,c=a.getSelectedRange().from;return a.getCellMeta(c.row,c.col).comment&&(b=!0),b}}}function f(b){var c,d=function(){a.localStorage[b+"__persistentStateKeys"]=JSON.stringify(c)},e=function(){var d=a.localStorage[b+"__persistentStateKeys"],e="string"==typeof d?JSON.parse(d):void 0;c=e?e:[]},f=function(){c=[],d()};e(),this.saveValue=function(e,f){a.localStorage[b+"_"+e]=JSON.stringify(f),-1==c.indexOf(e)&&(c.push(e),d())},this.loadValue=function(c,d){c="undefined"!=typeof c?c:d;var e=a.localStorage[b+"_"+c];return"undefined"==typeof e?void 0:JSON.parse(e)},this.reset=function(c){a.localStorage.removeItem(b+"_"+c)},this.resetAll=function(){for(var d=0;dc;c++)if(this[c].row<=a&&this[c].row+this[c].rowspan-1>=a&&this[c].col<=b&&this[c].col+this[c].colspan-1>=b)return this[c]},a.setInfo=function(a){for(var b=0,c=this.length;c>b;b++)if(this[b].row===a.row&&this[b].col===a.col)return void(this[b]=a);this.push(a)},a.removeInfo=function(a,b){for(var c=0,d=this.length;d>c;c++)if(this[c].row===a&&this[c].col===b){this.splice(c,1);break}},a}function i(a){if(this.mergedCellInfoCollection=new h,Array.isArray(a))for(var b=0,c=a.length;c>b;b++)this.mergedCellInfoCollection.setInfo(a[b])}function j(){}function k(a,c){var d,e=function(){this.selectionHandles={topLeft:document.createElement("DIV"),topLeftHitArea:document.createElement("DIV"),bottomRight:document.createElement("DIV"),bottomRightHitArea:document.createElement("DIV")};var a=10,b=40;this.selectionHandles.topLeft.className="topLeftSelectionHandle",this.selectionHandles.topLeftHitArea.className="topLeftSelectionHandle-HitArea",this.selectionHandles.bottomRight.className="bottomRightSelectionHandle",this.selectionHandles.bottomRightHitArea.className="bottomRightSelectionHandle-HitArea",this.selectionHandles.styles={topLeft:this.selectionHandles.topLeft.style,topLeftHitArea:this.selectionHandles.topLeftHitArea.style,bottomRight:this.selectionHandles.bottomRight.style,bottomRightHitArea:this.selectionHandles.bottomRightHitArea.style};var c={position:"absolute",height:b+"px",width:b+"px","border-radius":parseInt(b/1.5,10)+"px"};for(var d in c)c.hasOwnProperty(d)&&(this.selectionHandles.styles.bottomRightHitArea[d]=c[d],this.selectionHandles.styles.topLeftHitArea[d]=c[d]);var e={position:"absolute",height:a+"px",width:a+"px","border-radius":parseInt(a/1.5,10)+"px",background:"#F5F5FF",border:"1px solid #4285c8"};for(var d in e)e.hasOwnProperty(d)&&(this.selectionHandles.styles.bottomRight[d]=e[d],this.selectionHandles.styles.topLeft[d]=e[d]);this.main.appendChild(this.selectionHandles.topLeft),this.main.appendChild(this.selectionHandles.bottomRight),this.main.appendChild(this.selectionHandles.topLeftHitArea),this.main.appendChild(this.selectionHandles.bottomRightHitArea)};if(c){var f=b.eventManager(a);this.instance=a,this.settings=c,this.main=document.createElement("div"),d=this.main.style,d.position="absolute",d.top=0,d.left=0;for(var g=["top","left","bottom","right","corner"],h=0;5>h;h++){var i=g[h],j=document.createElement("DIV");j.className="wtBorder "+(this.settings.className||""),this.settings[i]&&this.settings[i].hide&&(j.className+=" hidden"),d=j.style,d.backgroundColor=this.settings[i]&&this.settings[i].color?this.settings[i].color:c.border.color,d.height=this.settings[i]&&this.settings[i].width?this.settings[i].width+"px":c.border.width+"px",d.width=this.settings[i]&&this.settings[i].width?this.settings[i].width+"px":c.border.width+"px",this.main.appendChild(j)}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.cornerDefaultStyle={width:"5px",height:"5px",borderWidth:"2px",borderStyle:"solid",borderColor:"#FFF"},this.corner=this.main.childNodes[4],this.corner.className+=" corner",this.cornerStyle=this.corner.style,this.cornerStyle.width=this.cornerDefaultStyle.width,this.cornerStyle.height=this.cornerDefaultStyle.height,this.cornerStyle.border=[this.cornerDefaultStyle.borderWidth,this.cornerDefaultStyle.borderStyle,this.cornerDefaultStyle.borderColor].join(" "),b.mobileBrowser&&e.call(this),this.disappear(),a.wtTable.bordersHolder||(a.wtTable.bordersHolder=document.createElement("div"),a.wtTable.bordersHolder.className="htBorders",a.wtTable.hider.appendChild(a.wtTable.bordersHolder)),a.wtTable.bordersHolder.insertBefore(this.main,a.wtTable.bordersHolder.firstChild);var k=!1;f.addEventListener(document.body,"mousedown",function(){k=!0}),f.addEventListener(document.body,"mouseup",function(){k=!1});for(var l=0,m=this.main.childNodes.length;m>l;l++)f.addEventListener(this.main.childNodes[l],"mouseenter",function(b){if(k&&a.getSetting("hideBorderOnMouseDownOver")){b.preventDefault(),b.stopImmediatePropagation();var c=this.getBoundingClientRect();this.style.display="none";var d=function(a){return a.clientYMath.ceil(c.top+c.height)?!0:a.clientXMath.ceil(c.left+c.width)?!0:void 0},e=function(a){d(a)&&(f.removeEventListener(document.body,"mousemove",e),this.style.display="block")};f.addEventListener(document.body,"mousemove",e)}})}}function l(a,b){"undefined"!=typeof a&&"undefined"!=typeof b?(this.row=a,this.col=b):(this.row=null,this.col=null)}function m(a,b,c){this.highlight=a,this.from=b,this.to=c}function n(a,b,c){this.offset=a,this.total=b,this.countTH=c}function o(a,b,c,d){var e,f=0;for(this.instance=a,this.containerSizeFn=b,this.cellSizesSum=0,this.cellSizes=[],this.cellStretch=[],this.cellCount=0,this.visibleCellCount=0,this.remainingSize=0,this.strategy=d;;){if(e=c(f),void 0===e)break;this.cellSizesSumd;d++)c.push(this.wtTable.THEAD.childNodes[0].childNodes[d].innerHTML);this.getSetting("columnHeaders").length||this.update("columnHeaders",[function(a,d){b.Dom.fastInnerText(d,c[a])}])}this.drawn=!1,this.drawInterrupted=!1}function q(a){this.instance=a,this.init(),this.clone=this.makeClone("debug"),this.clone.wtTable.holder.style.opacity=.4,this.clone.wtTable.holder.style.textShadow="0 0 2px #ff0000",this.lastTimeout=null,b.Dom.addClass(this.clone.wtTable.holder.parentNode,"wtDebugVisible")}function r(c){var d=this,e=b.eventManager(c);this.instance=c;var f=[null,null];this.dblClickTimeout=[null,null];var g,h,i=function(a){var c=d.parentCell(a.target);b.Dom.hasClass(a.target,"corner")?d.instance.getSetting("onCellCornerMouseDown",a,a.target):c.TD&&d.instance.hasSetting("onCellMouseDown")&&d.instance.getSetting("onCellMouseDown",a,c.coords,c.TD,d.instance),2!==a.button&&c.TD&&(f[0]=c.TD,clearTimeout(d.dblClickTimeout[0]),d.dblClickTimeout[0]=setTimeout(function(){f[0]=null},1e3))},j=function(){d.instance.touchMoving=!0},k=function(a){e.addEventListener(this,"touchmove",j),d.checkIfTouchMove=setTimeout(function(){return d.instance.touchMoving===!0?(d.instance.touchMoving=void 0,void e.removeEventListener("touchmove",j,!1)):void i(a)},30)},l=function(a){if(d.instance.hasSetting("onCellMouseOver")){var c=d.instance.wtTable.TABLE,e=b.Dom.closest(a.target,["TD","TH"],c);e&&e!==h&&b.Dom.isChildOf(e,c)&&(h=e,d.instance.getSetting("onCellMouseOver",a,d.instance.wtTable.getCoords(e),e,d.instance))}},m=function(a){if(2!==a.button){var c=d.parentCell(a.target);c.TD===f[0]&&c.TD===f[1]?(b.Dom.hasClass(a.target,"corner")?d.instance.getSetting("onCellCornerDblClick",a,c.coords,c.TD,d.instance):d.instance.getSetting("onCellDblClick",a,c.coords,c.TD,d.instance),f[0]=null,f[1]=null):c.TD===f[0]&&(f[1]=c.TD,clearTimeout(d.dblClickTimeout[1]),d.dblClickTimeout[1]=setTimeout(function(){f[1]=null},500))}},n=function(a){clearTimeout(g),a.preventDefault(),m(a)};if(e.addEventListener(this.instance.wtTable.holder,"mousedown",i),e.addEventListener(this.instance.wtTable.TABLE,"mouseover",l),e.addEventListener(this.instance.wtTable.holder,"mouseup",m),this.instance.wtTable.holder.parentNode.parentNode&&b.mobileBrowser){var o="."+this.instance.wtTable.holder.parentNode.className.split(" ").join(".");e.addEventListener(this.instance.wtTable.holder.parentNode.parentNode,"touchstart",function(a){d.instance.touchApplied=!0,b.Dom.isChildOf(a.target,o)&&k.call(a.target,a)}),e.addEventListener(this.instance.wtTable.holder.parentNode.parentNode,"touchend",function(a){d.instance.touchApplied=!1,b.Dom.isChildOf(a.target,o)&&n.call(a.target,a)}),d.instance.momentumScrolling||(d.instance.momentumScrolling={}),e.addEventListener(this.instance.wtTable.holder.parentNode.parentNode,"scroll",function(){clearTimeout(d.instance.momentumScrolling._timeout),d.instance.momentumScrolling.ongoing||d.instance.getSetting("onBeforeTouchScroll"),d.instance.momentumScrolling.ongoing=!0,d.instance.momentumScrolling._timeout=setTimeout(function(){d.instance.touchApplied||(d.instance.momentumScrolling.ongoing=!1,d.instance.getSetting("onAfterMomentumScroll"))},200)})}e.addEventListener(a,"resize",function(){d.instance.draw()}),this.destroy=function(){clearTimeout(this.dblClickTimeout[0]),clearTimeout(this.dblClickTimeout[1]),e.clear()}}function s(){function a(){return Math.floor(65536*(1+Math.random())).toString(16).substring(1)}return a()+a()+a()+a()}function t(a,b,c){this.offset=a,this.total=b,this.countTH=c}function u(a){this.instance=a}function v(a){this.instance=a,this.type="corner",this.init(),this.clone=this.makeClone("corner")}function w(a){this.instance=a,this.type="horizontal",this.offset=0,this.init(),this.clone=this.makeClone("left")}function x(a){this.instance=a,this.type="vertical",this.init(),this.clone=this.makeClone("top")}function y(a){this.instance=a,a.update("scrollbarWidth",b.Dom.getScrollbarWidth()),a.update("scrollbarHeight",b.Dom.getScrollbarWidth()),this.vertical=new x(a),this.horizontal=new w(a),this.corner=new v(a),a.getSetting("debug")&&(this.debug=new q(a)),this.registerListeners()}function z(a,b){this.settings=a,this.cellRange=b||null,this.instanceBorders={}}function A(a,c){var d=this;this.instance=a,this.defaults={table:void 0,debug:!1,stretchH:"none",currentRowClassName:null,currentColumnClassName:null,data:void 0,fixedColumnsLeft:0,fixedRowsTop:0,rowHeaders:function(){return[]},columnHeaders:function(){return[]},totalRows:void 0,totalColumns:void 0,cellRenderer:function(a,c,e){var f=d.getSetting("data",a,c);b.Dom.fastInnerText(e,void 0===f||null===f?"":f)},columnWidth:function(){},rowHeight:function(){},defaultRowHeight:23,defaultColumnWidth:50,selections:null,hideBorderOnMouseDownOver:!1,viewportRowCalculatorOverride:null,viewportColumnCalculatorOverride:null,onCellMouseDown:null,onCellMouseOver:null,onCellDblClick:null,onCellCornerMouseDown:null,onCellCornerDblClick:null,beforeDraw:null,onDraw:null,onBeforeDrawBorders:null,onScrollVertically:null,onScrollHorizontally:null,onBeforeTouchScroll:null,onAfterMomentumScroll:null,scrollbarWidth:10,scrollbarHeight:10,renderAllRows:!1,groups:!1},this.settings={};for(var e in this.defaults)if(this.defaults.hasOwnProperty(e))if(void 0!==c[e])this.settings[e]=c[e];else{if(void 0===this.defaults[e])throw new Error('A required setting "'+e+'" was not provided');this.settings[e]=this.defaults[e]}}function B(a,c){this.instance=a,this.TABLE=c,b.Dom.removeTextNodes(this.TABLE);var d=this.TABLE.parentNode;if(!d||1!==d.nodeType||!b.Dom.hasClass(d,"wtHolder")){var e=document.createElement("DIV");e.className="wtSpreader",d&&d.insertBefore(e,this.TABLE),e.appendChild(this.TABLE)}if(this.spreader=this.TABLE.parentNode,d=this.spreader.parentNode,!d||1!==d.nodeType||!b.Dom.hasClass(d,"wtHolder")){var f=document.createElement("DIV");f.className="wtHider",d&&d.insertBefore(f,this.spreader),f.appendChild(this.spreader)}if(this.hider=this.spreader.parentNode,this.hiderStyle=this.hider.style,this.hiderStyle.position="relative",d=this.hider.parentNode,!d||1!==d.nodeType||!b.Dom.hasClass(d,"wtHolder")){var g=document.createElement("DIV");g.style.position="relative",g.className="wtHolder",a.cloneSource||(g.className+=" ht_master"),d&&d.insertBefore(g,this.hider),g.appendChild(this.hider)}if(this.holder=this.hider.parentNode,this.isWorkingOnClone()||(this.holder.parentNode.style.position="relative"),this.TBODY=this.TABLE.getElementsByTagName("TBODY")[0],this.TBODY||(this.TBODY=document.createElement("TBODY"),this.TABLE.appendChild(this.TBODY)),this.THEAD=this.TABLE.getElementsByTagName("THEAD")[0],this.THEAD||(this.THEAD=document.createElement("THEAD"),this.TABLE.insertBefore(this.THEAD,this.TBODY)),this.COLGROUP=this.TABLE.getElementsByTagName("COLGROUP")[0],this.COLGROUP||(this.COLGROUP=document.createElement("COLGROUP"),this.TABLE.insertBefore(this.COLGROUP,this.THEAD)),this.instance.getSetting("columnHeaders").length&&!this.THEAD.childNodes.length){var h=document.createElement("TR");this.THEAD.appendChild(h)}this.colgroupChildrenLength=this.COLGROUP.childNodes.length,this.theadChildrenLength=this.THEAD.firstChild?this.THEAD.firstChild.childNodes.length:0,this.tbodyChildrenLength=this.TBODY.childNodes.length,this.rowFilter=null,this.columnFilter=null}function C(a){this.wtTable=a,this.instance=a.instance,this.rowFilter=a.rowFilter,this.columnFilter=a.columnFilter,this.TABLE=a.TABLE,this.THEAD=a.THEAD,this.TBODY=a.TBODY,this.COLGROUP=a.COLGROUP,this.utils=C.utils}function D(c){this.instance=c,this.oversizedRows=[],this.oversizedCols=[],this.oversizedColumnHeaders=[];var d=this,e=b.eventManager(c);e.addEventListener(a,"resize",function(){d.clientHeight=d.getWorkspaceHeight()})}function E(a,b,c,d,e,f,g){function h(a,b){var d=0;if(m.stretchAllColumnsWidth[a]||(m.stretchAllColumnsWidth[a]=Math.round(b*m.stretchAllRatio)),m.stretchAllColumnsWidth.length===c&&m.needVerifyLastColumnWidth){m.needVerifyLastColumnWidth=!1;for(var e=0;ef;f++)b=j(f),e+=b;this.totalTargetWidth=a,d=e-a,"all"===this.stretch&&0>d?(this.stretchAllRatio=a/e,this.stretchAllColumnsWidth=[],this.needVerifyLastColumnWidth=!0):"last"===this.stretch&&1/0!==a&&(this.stretchLastWidth=-d+j(c-1))},this.getStretchedColumnWidth=function(a,b){var c=null;return"all"===this.stretch&&0!==this.stretchAllRatio?c=h(a,b):"last"===this.stretch&&0!==this.stretchLastWidth&&(c=i(a,b)),c},l=0;c>l;l++)if(k=j(l),b>=o&&!f&&(this.startColumn=l),o>=b&&b+a>=o+k&&(null==this.startColumn&&(this.startColumn=l),this.endColumn=l),r.push(o),o+=k,f||(this.endColumn=l),o>=b+a){p=!1;break}if(this.endColumn==c-1&&p)for(this.startColumn=this.endColumn;this.startColumn>0;){var s=r[this.endColumn]+k-r[this.startColumn-1];if((a>=s||!f)&&this.startColumn--,s>a)break}null!==this.startColumn&&e&&e(this),this.startPosition=r[this.startColumn],void 0==this.startPosition&&(this.startPosition=null),null!=this.startColumn&&(this.count=this.endColumn-this.startColumn+1)}function F(a,b,c,d,e,f){this.scrollOffset=b,this.startRow=null,this.startPosition=null,this.endRow=null,this.count=0;for(var g,h=0,i=!0,j=23,k=[],l=0;c>l;l++)if(g=d(l),void 0===g&&(g=j),b>=h&&!f&&(this.startRow=l),h>=b&&b+a>=h+g&&(null==this.startRow&&(this.startRow=l),this.endRow=l),k.push(h),h+=g,f||(this.endRow=l),h>=b+a){i=!1;break}if(this.endRow==c-1&&i)for(this.startRow=this.endRow;this.startRow>0;){var m=k[this.endRow]+g-k[this.startRow-1];if((a>=m||!f)&&this.startRow--,m>=a)break}null!==this.startRow&&e&&e(this),this.startPosition=k[this.startRow],void 0==this.startPosition&&(this.startPosition=null),null!=this.startRow&&(this.count=this.endRow-this.startRow+1)}Array.prototype.indexOf||(Array.prototype.indexOf=function(a){var b=this.length>>>0,c=Number(arguments[1])||0;for(c=0>c?Math.ceil(c):Math.floor(c),0>c&&(c+=b);b>c;c++)if(c in this&&this[c]===a)return c;return-1}),Array.prototype.filter||(Array.prototype.filter=function(a,b){function c(a){return/NodeList/i.test(a.item)}function d(a){for(var b=[],c=0,d=a.length;d>c;c++)b[c]=a[c];return b}if("undefined"==typeof this||null===this)throw new TypeError;if("function"!=typeof a)throw new TypeError;b=b||this,c(b)&&(b=d(b));var e,f,g=b.length,h=[];for(e=0;g>e;e+=1)b.hasOwnProperty(e)&&(f=b[e],a.call(b,f,e,b)&&h.push(f));return h}),Array.isArray||(Array.isArray=function(a){return"[object Array]"==toString.call(a)}),Object.keys||(Object.keys=function(){var a=Object.prototype.hasOwnProperty,b=!{toString:null}.propertyIsEnumerable("toString"),c=["toString","toLocaleString","valueOf","hasOwnProperty","isPrototypeOf","propertyIsEnumerable","constructor"],d=c.length;return function(e){if("object"!=typeof e&&("function"!=typeof e||null===e))throw new TypeError("Object.keys called on non-object");var f,g,h=[];for(f in e)a.call(e,f)&&h.push(f);if(b)for(g=0;d>g;g++)a.call(e,c[g])&&h.push(c[g]);return h}}()),"undefined"==typeof WeakMap&&!function(){var b=Object.defineProperty;try{var c=!0;b(function(){},"foo",{})}catch(d){c=!1}var e=+new Date%1e9,f=function(){this.name="__st"+(1e9*Math.random()>>>0)+(e++ +"__"),c||(this._wmCache=[])};f.prototype=c?{set:function(a,c){var d=a[this.name];d&&d[0]===a?d[1]=c:b(a,this.name,{value:[a,c],writable:!0})},get:function(a){var b;return(b=a[this.name])&&b[0]===a?b[1]:void 0},"delete":function(a){this.set(a,void 0)}}:{set:function(a,b){if("undefined"!=typeof a&&"undefined"!=typeof b){for(var c=0,d=this._wmCache.length;d>c;c++)if(this._wmCache[c].key==a)return void(this._wmCache[c].value=b);this._wmCache.push({key:a,value:b})}},get:function(a){if("undefined"!=typeof a)for(var b=0,c=this._wmCache.length;c>b;b++)if(this._wmCache[b].key==a)return this._wmCache[b].value},"delete":function(a){if("undefined"!=typeof a)for(var b=0,c=this._wmCache.length;c>b;b++)this._wmCache[b].key==a&&Array.prototype.slice.call(this._wmCache,b,1)}},a.WeakMap=f}(),b.activeGuid=null,b.Core=function(a,c){function d(){var a=!1;return{validatorsInQueue:0,addValidatorToQueue:function(){this.validatorsInQueue++,a=!1},removeValidatorFormQueue:function(){this.validatorsInQueue=this.validatorsInQueue-1<0?0:this.validatorsInQueue-1,this.checkIfQueueIsEmpty()},onQueueEmpty:function(){},checkIfQueueIsEmpty:function(){0==this.validatorsInQueue&&0==a&&(a=!0,this.onQueueEmpty())}}}function e(a,c,e){function f(){var d;a.length&&(d=b.hooks.run(s,"beforeChange",a,c),"function"==typeof d?console.warn("Your beforeChange callback returns a function. It's not supported since Handsontable 0.12.1 (and the returned function will not be executed)."):d===!1&&a.splice(0,a.length)),e()}var g=new d;g.onQueueEmpty=f;for(var h=a.length-1;h>=0;h--)if(null===a[h])a.splice(h,1);else{var i=a[h][0],j=o.propToCol(a[h][1]),k=s.runHooks("modifyCol",j),l=s.getCellMeta(i,k);if("numeric"===l.type&&"string"==typeof a[h][3]&&a[h][3].length>0&&(/^-?[\d\s]*(\.|\,)?\d*$/.test(a[h][3])||l.format)){var m=a[h][3].length;numeral.language("undefined"==typeof l.language?"en":a[h][3].indexOf(".")===m-3&&-1===a[h][3].indexOf(",")?"en":l.language),numeral.validate(a[h][3])&&(a[h][3]=numeral().unformat(a[h][3]))}s.getCellValidator(l)&&(g.addValidatorToQueue(),s.validateCell(a[h][3],l,function(b,c){return function(d){if("boolean"!=typeof d)throw new Error("Validation error: result is not boolean");d===!1&&c.allowInvalid===!1&&(a.splice(b,1),c.valid=!0,--b),g.removeValidatorFormQueue()}}(h,l),c))}g.checkIfQueueIsEmpty()}function f(a,c){var d=a.length-1;if(!(0>d)){for(;d>=0;d--)if(null!==a[d]){if(null!=a[d][2]||null!=a[d][3]){if(n.settings.allowInsertRow)for(;a[d][0]>s.countRows()-1;)o.createRow();if("array"===s.dataType&&n.settings.allowInsertColumn)for(;o.propToCol(a[d][1])>s.countCols()-1;)o.createCol();o.set(a[d][0],a[d][1],a[d][3])}}else a.splice(d,1);s.forceFullRender=!0,p.adjustRowsAndCols(),b.hooks.run(s,"beforeChangeRender",a,c),q.refreshBorders(null,!0),b.hooks.run(s,"afterChange",a,c||"edit")}}function g(a,b,c){return"object"==typeof a?a:[[a,b,c]]}function h(a){if(a.hasOwnProperty("type")){var c,d={};if("object"==typeof a.type)c=a.type;else if("string"==typeof a.type&&(c=b.cellTypes[a.type],void 0===c))throw new Error('You declared cell type "'+a.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 e in c)c.hasOwnProperty(e)&&!a.hasOwnProperty(e)&&(d[e]=c[e]);return d}}function i(a){return b.hooks.run(s,"modifyRow",a)}function j(a){return b.hooks.run(s,"modifyCol",a)}function k(){throw new Error("This method cannot be called because this Handsontable instance has been destroyed")}var n,o,p,q,r,s=this,t=function(){},u=b.eventManager(s);b.helper.extend(t.prototype,G.prototype),b.helper.extend(t.prototype,c),b.helper.extend(t.prototype,h(c)),this.rootElement=a,this.container=document.createElement("DIV"),this.container.className="htContainer",a.insertBefore(this.container,a.firstChild),this.guid="ht_"+b.helper.randomString(),this.rootElement.id&&"ht_"!==this.rootElement.id.substring(0,3)||(this.rootElement.id=this.guid),n={cellSettings:[],columnSettings:[],columnsSettingConflicts:["data","width"],settings:new t,selRange:null,isPopulated:null,scrollable:null,firstRun:!0},p={alter:function(a,b,c,d,e){var f;switch(c=c||1,a){case"insert_row":f=o.createRow(b,c),f&&(q.isSelected()&&n.selRange.from.row>=b?(n.selRange.from.row=n.selRange.from.row+f,q.transformEnd(f,0)):q.refreshBorders());break;case"insert_col":if(f=o.createCol(b,c)){if(Array.isArray(s.getSettings().colHeaders)){var g=[b,0];g.length+=f,Array.prototype.splice.apply(s.getSettings().colHeaders,g)}q.isSelected()&&n.selRange.from.col>=b?(n.selRange.from.col=n.selRange.from.col+f,q.transformEnd(0,f)):q.refreshBorders()}break;case"remove_row":b=s.runHooks("modifyCol",b),o.removeRow(b,c),n.cellSettings.splice(b,c),p.adjustRowsAndCols(),q.refreshBorders();break;case"remove_col":o.removeCol(b,c);for(var h=0,i=o.getAll().length;i>h;h++)h in n.cellSettings&&n.cellSettings[h].splice(b,c);Array.isArray(s.getSettings().colHeaders)&&("undefined"==typeof b&&(b=-1),s.getSettings().colHeaders.splice(b,c)),p.adjustRowsAndCols(),q.refreshBorders();break;default:throw new Error('There is no such action "'+a+'"')}e||p.adjustRowsAndCols()},adjustRowsAndCols:function(){var a,b,c,d;if(b=s.countRows(),be-1?(h=e-1,g=!0,j>h&&(j=h)):j>e-1&&(j=e-1,g=!0,h>j&&(h=j)),i>f-1?(i=f-1,g=!0,k>i&&(k=i)):k>f-1&&(k=f-1,g=!0,i>k&&(i=k)),g&&s.selectCell(h,i,j,k)}},populateFromArray:function(a,c,d,e,f,g,h){var i,j,k,l,m=[],o={};if(j=c.length,0===j)return!1;var p,q,r,t;switch(f){case"shift_down":for(p=d?d.col-a.col+1:0,q=d?d.row-a.row+1:0,c=b.helper.translateRowsToColumns(c),k=0,l=c.length,r=Math.max(l,p);r>k;k++)if(l>k){for(i=0,j=c[k].length;q-j>i;i++)c[k].push(c[k][i%j]);c[k].unshift(a.col+k,a.row,0),s.spliceCol.apply(s,c[k])}else c[k%l][0]=a.col+k,s.spliceCol.apply(s,c[k%l]);break;case"shift_right":for(p=d?d.col-a.col+1:0,q=d?d.row-a.row+1:0,i=0,j=c.length,t=Math.max(j,q);t>i;i++)if(j>i){for(k=0,l=c[i].length;p-l>k;k++)c[i].push(c[i][k%l]);c[i].unshift(a.row+i,a.col,0),s.spliceRow.apply(s,c[i])}else c[i%j][0]=a.row+i,s.spliceRow.apply(s,c[i%j]);break;case"overwrite":default:o.row=a.row,o.col=a.col;var u={row:0,col:0},v={row:d&&a?d.row-a.row+1:1,col:d&&a?d.col-a.col+1:1};for(-1!==["up","left"].indexOf(g)?u={row:Math.ceil(v.row/j)||1,col:Math.ceil(v.col/c[0].length)||1}:-1!==["down","right"].indexOf(g)&&(u={row:1,col:1}),i=0;j>i&&!(d&&o.row>d.row||!n.settings.allowInsertRow&&o.row>s.countRows()-1||o.row>=n.settings.maxRows);i++){for(o.col=a.col,l=c[i]?c[i].length:0,k=0;l>k&&!(d&&o.col>d.col||!n.settings.allowInsertColumn&&o.col>s.countCols()-1||o.col>=n.settings.maxCols);k++){if(!s.getCellMeta(o.row,o.col).readOnly){var w,x=c[i][k],y={row:i,col:k}; +"autofill"===e&&(w=s.runHooks("beforeAutofillInsidePopulate",y,g,c,h,u,v),w&&(u="undefined"!=typeof w.iterators?w.iterators:u,x="undefined"!=typeof w.value?w.value:x)),m.push([o.row,o.col,x])}o.col++,d&&k===l-1&&(k=-1,-1!==["down","right"].indexOf(g)?u.col++:-1!==["up","left"].indexOf(g)&&u.col>1&&u.col--)}o.row++,u.col=1,d&&i===j-1&&(i=-1,-1!==["down","right"].indexOf(g)?u.row++:-1!==["up","left"].indexOf(g)&&u.row>1&&u.row--)}s.setDataAtCell(m,null,null,e||"populateFromArray")}}},this.selection=q={inProgress:!1,selectedHeader:{cols:!1,rows:!1},setSelectedHeaders:function(a,b){s.selection.selectedHeader.rows=a,s.selection.selectedHeader.cols=b},begin:function(){s.selection.inProgress=!0},finish:function(){var a=s.getSelected();b.hooks.run(s,"afterSelectionEnd",a[0],a[1],a[2],a[3]),b.hooks.run(s,"afterSelectionEndByProp",a[0],s.colToProp(a[1]),a[2],s.colToProp(a[3])),s.selection.inProgress=!1},isInProgress:function(){return s.selection.inProgress},setRangeStart:function(a,c){b.hooks.run(s,"beforeSetRangeStart",a),n.selRange=new m(a,a,a),q.setRangeEnd(a,null,c)},setRangeEnd:function(a,c,d){b.hooks.run(s,"beforeSetRangeEnd",a),s.selection.begin(),n.selRange.to=new l(a.row,a.col),n.settings.multiSelect||(n.selRange.from=a),s.view.wt.selections.current.clear(),s.view.wt.selections.current.add(n.selRange.highlight),s.view.wt.selections.area.clear(),q.isMultiple()&&(s.view.wt.selections.area.add(n.selRange.from),s.view.wt.selections.area.add(n.selRange.to)),(n.settings.currentRowClassName||n.settings.currentColClassName)&&(s.view.wt.selections.highlight.clear(),s.view.wt.selections.highlight.add(n.selRange.from),s.view.wt.selections.highlight.add(n.selRange.to)),b.hooks.run(s,"afterSelection",n.selRange.from.row,n.selRange.from.col,n.selRange.to.row,n.selRange.to.col),b.hooks.run(s,"afterSelectionByProp",n.selRange.from.row,o.colToProp(n.selRange.from.col),n.selRange.to.row,o.colToProp(n.selRange.to.col)),c!==!1&&s.view.mainViewIsActive()&&s.view.scrollViewport(n.selRange.from?n.selRange.from:a),q.refreshBorders(null,d)},refreshBorders:function(a,b){b||r.destroyEditor(a),s.view.render(),q.isSelected()&&!b&&r.prepareEditor()},isMultiple:function(){var a=!(n.selRange.to.col===n.selRange.from.col&&n.selRange.to.row===n.selRange.from.row),c=b.hooks.run(s,"afterIsMultipleSelection",a);return a?c:void 0},transformStart:function(a,b,c,d){var e=new l(a,b);s.runHooks("modifyTransformStart",e),n.selRange.highlight.row+a>s.countRows()-1?c&&n.settings.minSpareRows>0?s.alter("insert_row",s.countRows()):n.settings.autoWrapCol&&(e.row=1-s.countRows(),e.col=n.selRange.highlight.col+e.col==s.countCols()-1?1-s.countCols():1):n.settings.autoWrapCol&&n.selRange.highlight.row+e.row<0&&n.selRange.highlight.col+e.col>=0&&(e.row=s.countRows()-1,e.col=n.selRange.highlight.col+e.col==0?s.countCols()-1:-1),n.selRange.highlight.col+e.col>s.countCols()-1?c&&n.settings.minSpareCols>0?s.alter("insert_col",s.countCols()):n.settings.autoWrapRow&&(e.row=n.selRange.highlight.row+e.row==s.countRows()-1?1-s.countRows():1,e.col=1-s.countCols()):n.settings.autoWrapRow&&n.selRange.highlight.col+e.col<0&&n.selRange.highlight.row+e.row>=0&&(e.row=n.selRange.highlight.row+e.row==0?s.countRows()-1:-1,e.col=s.countCols()-1);var f=s.countRows(),g=s.countCols(),h=new l(n.selRange.highlight.row+e.row,n.selRange.highlight.col+e.col);h.row<0?h.row=0:h.row>0&&h.row>=f&&(h.row=f-1),h.col<0?h.col=0:h.col>0&&h.col>=g&&(h.col=g-1),q.setRangeStart(h,d)},transformEnd:function(a,b){var c=new l(a,b);s.runHooks("modifyTransformEnd",c);var d=s.countRows(),e=s.countCols(),f=new l(n.selRange.to.row+c.row,n.selRange.to.col+c.col);f.row<0?f.row=0:f.row>0&&f.row>=d&&(f.row=d-1),f.col<0?f.col=0:f.col>0&&f.col>=e&&(f.col=e-1),q.setRangeEnd(f)},isSelected:function(){return null!==n.selRange},inInSelection:function(a){return q.isSelected()?n.selRange.includes(a):!1},deselect:function(){q.isSelected()&&(s.selection.inProgress=!1,n.selRange=null,s.view.wt.selections.current.clear(),s.view.wt.selections.area.clear(),(n.settings.currentRowClassName||n.settings.currentColClassName)&&s.view.wt.selections.highlight.clear(),r.destroyEditor(),q.refreshBorders(),b.hooks.run(s,"afterDeselect"))},selectAll:function(){n.settings.multiSelect&&(q.setRangeStart(new l(0,0)),q.setRangeEnd(new l(s.countRows()-1,s.countCols()-1),!1))},empty:function(){if(q.isSelected()){var a,b,c=n.selRange.getTopLeftCorner(),d=n.selRange.getBottomRightCorner(),e=[];for(a=c.row;a<=d.row;a++)for(b=c.col;b<=d.col;b++)s.getCellMeta(a,b).readOnly||e.push([a,b,""]);s.setDataAtCell(e)}}},this.init=function(){b.hooks.run(s,"beforeInit"),b.mobileBrowser&&b.Dom.addClass(s.rootElement,"mobile"),this.updateSettings(n.settings,!0),this.view=new b.TableView(this),r=new b.EditorManager(s,n,q,o),this.forceFullRender=!0,this.view.render(),"object"==typeof n.firstRun&&(b.hooks.run(s,"afterChange",n.firstRun[0],n.firstRun[1]),n.firstRun=!1),b.hooks.run(s,"afterInit")},this.validateCell=function(a,c,d,e){var f=s.getCellValidator(c);"[object RegExp]"===Object.prototype.toString.call(f)&&(f=function(a){return function(b,c){c(a.test(b))}}(f)),"function"==typeof f?(a=b.hooks.run(s,"beforeValidate",a,c.row,c.prop,e),s._registerTimeout(setTimeout(function(){f.call(c,a,function(f){f=b.hooks.run(s,"afterValidate",f,a,c.row,c.prop,e),c.valid=f,d(f),b.hooks.run(s,"postAfterValidate",f,a,c.row,c.prop,e)})},0))):(c.valid=!0,d(!0))},this.setDataAtCell=function(a,b,c,d){var h,i,j,k=g(a,b,c),l=[];for(h=0,i=k.length;i>h;h++){if("object"!=typeof k[h])throw new Error("Method `setDataAtCell` accepts row number or changes array of arrays as its first parameter");if("number"!=typeof k[h][1])throw new Error("Method `setDataAtCell` accepts row and column number as its parameters. If you want to use object property name, use method `setDataAtRowProp`");j=o.colToProp(k[h][1]),l.push([k[h][0],j,o.get(k[h][0],j),k[h][2]])}d||"object"!=typeof a||(d=b),e(l,d,function(){f(l,d)})},this.setDataAtRowProp=function(a,b,c,d){var h,i,j=g(a,b,c),k=[];for(h=0,i=j.length;i>h;h++)k.push([j[h][0],j[h][1],o.get(j[h][0],j[h][1]),j[h][2]]);d||"object"!=typeof a||(d=b),e(k,d,function(){f(k,d)})},this.listen=function(){b.activeGuid=s.guid,document.activeElement&&document.activeElement!==document.body?document.activeElement.blur():document.activeElement||document.body.focus()},this.unlisten=function(){b.activeGuid=null},this.isListening=function(){return b.activeGuid===s.guid},this.destroyEditor=function(a){q.refreshBorders(a)},this.populateFromArray=function(a,b,c,d,e,f,g,h,i){var j;if("object"!=typeof c||"object"!=typeof c[0])throw new Error("populateFromArray parameter `input` must be an array of arrays");return j="number"==typeof d?new l(d,e):null,p.populateFromArray(new l(a,b),c,j,f,g,h,i)},this.spliceCol=function(){return o.spliceCol.apply(o,arguments)},this.spliceRow=function(){return o.spliceRow.apply(o,arguments)},this.getSelected=function(){return q.isSelected()?[n.selRange.from.row,n.selRange.from.col,n.selRange.to.row,n.selRange.to.col]:void 0},this.getSelectedRange=function(){return q.isSelected()?n.selRange:void 0},this.render=function(){s.view&&(s.forceFullRender=!0,q.refreshBorders(null,!0))},this.loadData=function(a){function c(){n.cellSettings.length=0}if("object"==typeof a&&null!==a)a.push&&a.splice||(a=[a]);else{if(null!==a)throw new Error("loadData only accepts array of objects or array of arrays ("+typeof a+" given)");a=[];for(var d,e=0,f=n.settings.startRows;f>e;e++){d=[];for(var g=0,h=n.settings.startCols;h>g;g++)d.push(null);a.push(d)}}n.isPopulated=!1,t.prototype.data=a,s.dataType=Array.isArray(n.settings.dataSchema)||Array.isArray(a[0])?"array":"function"==typeof n.settings.dataSchema?"function":"object",o=new b.DataMap(s,n,t),c(),p.adjustRowsAndCols(),b.hooks.run(s,"afterLoadData"),n.firstRun?n.firstRun=[null,"loadData"]:(b.hooks.run(s,"afterChange",null,"loadData"),s.render()),n.isPopulated=!0},this.getData=function(a,b,c,d){return"undefined"==typeof a?o.getAll():o.getRange(new l(a,b),new l(c,d),o.DESTINATION_RENDERER)},this.getCopyableData=function(a,b,c,d){return o.getCopyableText(new l(a,b),new l(c,d))},this.updateSettings=function(a,c){var d,e;if("undefined"!=typeof a.rows)throw new Error("'rows' setting is no longer supported. do you mean startRows, minRows or maxRows?");if("undefined"!=typeof a.cols)throw new Error("'cols' setting is no longer supported. do you mean startCols, minCols or maxCols?");for(d in a)"data"!==d&&(void 0!==b.hooks.hooks[d]||void 0!==b.hooks.legacy[d]?("function"==typeof a[d]||Array.isArray(a[d]))&&s.addHook(d,a[d]):!c&&a.hasOwnProperty(d)&&(t.prototype[d]=a[d]));if(void 0===a.data&&void 0===n.settings.data?s.loadData(null):void 0!==a.data?s.loadData(a.data):void 0!==a.columns&&o.createMap(),e=s.countCols(),n.cellSettings.length=0,e>0){var f,g;for(d=0;e>d;d++)n.columnSettings[d]=b.helper.columnFactory(t,n.columnsSettingConflicts),f=n.columnSettings[d].prototype,t.prototype.columns&&(g=t.prototype.columns[d],b.helper.extend(f,g),b.helper.extend(f,h(g)))}if("undefined"!=typeof a.cell)for(d in a.cell){var i=a.cell[d];s.setCellMetaObject(i.row,i.col,i)}if(b.hooks.run(s,"afterCellMetaReset"),"undefined"!=typeof a.className&&(t.prototype.className&&b.Dom.removeClass(s.rootElement,t.prototype.className),a.className&&b.Dom.addClass(s.rootElement,a.className)),"undefined"!=typeof a.height){var j=a.height;"function"==typeof j&&(j=j()),s.rootElement.style.height=j+"px"}if("undefined"!=typeof a.width){var k=a.width;"function"==typeof k&&(k=k()),s.rootElement.style.width=k+"px"}j&&(s.rootElement.style.overflow="auto"),c||b.hooks.run(s,"afterUpdateSettings"),p.adjustRowsAndCols(),s.view&&!n.firstRun&&(s.forceFullRender=!0,q.refreshBorders(null,!0))},this.getValue=function(){var a=s.getSelected();if(t.prototype.getValue){if("function"==typeof t.prototype.getValue)return t.prototype.getValue.call(s);if(a)return s.getData()[a[0]][t.prototype.getValue]}else if(a)return s.getDataAtCell(a[0],a[1])},this.getSettings=function(){return n.settings},this.clear=function(){q.selectAll(),q.empty()},this.alter=function(a,b,c,d,e){p.alter(a,b,c,d,e)},this.getCell=function(a,b,c){return s.view.getCellAtCoords(new l(a,b),c)},this.getCoords=function(a){return this.view.wt.wtTable.getCoords.call(this.view.wt.wtTable,a)},this.colToProp=function(a){return o.colToProp(a)},this.propToCol=function(a){return o.propToCol(a)},this.getDataAtCell=function(a,b){return o.get(a,o.colToProp(b))},this.getDataAtRowProp=function(a,b){return o.get(a,b)},this.getDataAtCol=function(a){var b=[];return b.concat.apply(b,o.getRange(new l(0,a),new l(n.settings.data.length-1,a),o.DESTINATION_RENDERER))},this.getDataAtProp=function(a){var b,c=[];return b=o.getRange(new l(0,o.propToCol(a)),new l(n.settings.data.length-1,o.propToCol(a)),o.DESTINATION_RENDERER),c.concat.apply(c,b)},this.getSourceDataAtCol=function(a){for(var b=[],c=n.settings.data,d=0;d=0;){for(var e=s.countCols()-1;e>=0;)b.addValidatorToQueue(),s.validateCell(s.getDataAtCell(c,e),s.getCellMeta(c,e),function(){b.removeValidatorFormQueue()},"validateCells"),e--;c--}b.checkIfQueueIsEmpty()},this.getRowHeader=function(a){if(void 0===a){for(var b=[],c=0,d=s.countRows();d>c;c++)b.push(s.getRowHeader(c));return b}return Array.isArray(n.settings.rowHeaders)&&void 0!==n.settings.rowHeaders[a]?n.settings.rowHeaders[a]:"function"==typeof n.settings.rowHeaders?n.settings.rowHeaders(a):n.settings.rowHeaders&&"string"!=typeof n.settings.rowHeaders&&"number"!=typeof n.settings.rowHeaders?a+1:n.settings.rowHeaders},this.hasRowHeaders=function(){return!!n.settings.rowHeaders},this.hasColHeaders=function(){if(void 0!==n.settings.colHeaders&&null!==n.settings.colHeaders)return!!n.settings.colHeaders;for(var a=0,b=s.countCols();b>a;a++)if(s.getColHeader(a))return!0;return!1},this.getColHeader=function(a){if(void 0===a){for(var c=[],d=0,e=s.countCols();e>d;d++)c.push(s.getColHeader(d));return c}var f=a;return a=b.hooks.run(s,"modifyCol",a),n.settings.columns&&n.settings.columns[a]&&n.settings.columns[a].title?n.settings.columns[a].title:Array.isArray(n.settings.colHeaders)&&void 0!==n.settings.colHeaders[a]?n.settings.colHeaders[a]:"function"==typeof n.settings.colHeaders?n.settings.colHeaders(a):n.settings.colHeaders&&"string"!=typeof n.settings.colHeaders&&"number"!=typeof n.settings.colHeaders?b.helper.spreadsheetColumnLabel(f):n.settings.colHeaders},this._getColWidthFromSettings=function(a){var b=s.getCellMeta(0,a),c=b.width;if((void 0===c||c===n.settings.width)&&(c=b.colWidths),void 0!==c&&null!==c){switch(typeof c){case"object":c=c[a];break;case"function":c=c(a)}"string"==typeof c&&(c=parseInt(c,10))}return c},this.getColWidth=function(a){var c=s._getColWidthFromSettings(a);return c||(c=50),c=b.hooks.run(s,"modifyColWidth",c,a)},this._getRowHeightFromSettings=function(a){var b=n.settings.rowHeights;if(void 0!==b&&null!==b){switch(typeof b){case"object":b=b[a];break;case"function":b=b(a)}"string"==typeof b&&(b=parseInt(b,10))}return b},this.getRowHeight=function(a){var c=s._getRowHeightFromSettings(a);return c=b.hooks.run(s,"modifyRowHeight",c,a)},this.countRows=function(){return n.settings.data.length},this.countCols=function(){return"object"===s.dataType||"function"===s.dataType?n.settings.columns&&n.settings.columns.length?n.settings.columns.length:o.colToPropCache.length:"array"===s.dataType?n.settings.columns&&n.settings.columns.length?n.settings.columns.length:n.settings.data&&n.settings.data[0]&&n.settings.data[0].length?n.settings.data[0].length:0:void 0},this.rowOffset=function(){return s.view.wt.wtTable.getFirstRenderedRow()},this.colOffset=function(){return s.view.wt.wtTable.getFirstRenderedColumn()},this.countRenderedRows=function(){return s.view.wt.drawn?s.view.wt.wtTable.getRenderedRowsCount():-1},this.countVisibleRows=function(){return s.view.wt.drawn?s.view.wt.wtTable.getVisibleRowsCount():-1},this.countRenderedCols=function(){return s.view.wt.drawn?s.view.wt.wtTable.getRenderedColumnsCount():-1},this.countVisibleCols=function(){return s.view.wt.drawn?s.view.wt.wtTable.getVisibleColumnsCount():-1},this.countEmptyRows=function(a){for(var c,d=s.countRows()-1,e=0;d>=0;){if(c=b.hooks.run(this,"modifyRow",d),s.isEmptyRow(c))e++;else if(a)break;d--}return e},this.countEmptyCols=function(a){if(s.countRows()<1)return 0;for(var b=s.countCols()-1,c=0;b>=0;){if(s.isEmptyCol(b))c++;else if(a)break;b--}return c},this.isEmptyRow=function(a){return n.settings.isEmptyRow.call(s,a)},this.isEmptyCol=function(a){return n.settings.isEmptyCol.call(s,a)},this.selectCell=function(a,b,c,d,e){if("number"!=typeof a||0>a||a>=s.countRows())return!1;if("number"!=typeof b||0>b||b>=s.countCols())return!1;if("undefined"!=typeof c){if("number"!=typeof c||0>c||c>=s.countRows())return!1;if("number"!=typeof d||0>d||d>=s.countCols())return!1}var f=new l(a,b);return n.selRange=new m(f,f,f),document.activeElement&&document.activeElement!==document.documentElement&&document.activeElement!==document.body&&document.activeElement.blur(),s.listen(),"undefined"==typeof c?q.setRangeEnd(n.selRange.from,e):q.setRangeEnd(new l(c,d),e),s.selection.finish(),!0},this.selectCellByProp=function(){return arguments[1]=o.propToCol(arguments[1]),"undefined"!=typeof arguments[3]&&(arguments[3]=o.propToCol(arguments[3])),s.selectCell.apply(s,arguments)},this.deselectCell=function(){q.deselect()},this.destroy=function(){s._clearTimeouts(),s.view&&s.view.destroy(),b.Dom.empty(s.rootElement),u.clear(),b.hooks.run(s,"afterDestroy"),b.hooks.destroy(s);for(var a in s)s.hasOwnProperty(a)&&("function"==typeof s[a]?"runHooks"!==a&&(s[a]=k):"guid"!==a&&(s[a]=null));n=null,o=null,p=null,q=null,r=null,s=null,t=null},this.getActiveEditor=function(){return r.getActiveEditor()},this.getInstance=function(){return s},this.addHook=function(a,c){b.hooks.add(a,c,s)},this.addHookOnce=function(a,c){b.hooks.once(a,c,s)},this.removeHook=function(a,c){b.hooks.remove(a,c,s)},this.runHooks=function(a,c,d,e,f,g,h){return b.hooks.run(s,a,c,d,e,f,g,h)},this.timeouts=[],this._registerTimeout=function(a){this.timeouts.push(a)},this._clearTimeouts=function(){for(var a=0,b=this.timeouts.length;b>a;a++)clearTimeout(this.timeouts[a])},this.version="0.12.4"};var G=function(){};if(G.prototype={data:void 0,dataSchema:void 0,width:void 0,height:void 0,startRows:5,startCols:5,rowHeaders:null,colHeaders:null,colWidths:void 0,columns:void 0,cells:void 0,cell:[],minRows:0,minCols:0,maxRows:1/0,maxCols:1/0,minSpareRows:0,minSpareCols:0,allowInsertRow:!0,allowInsertColumn:!0,allowRemoveRow:!0,allowRemoveColumn:!0,multiSelect:!0,fillHandle:!0,fixedRowsTop:0,fixedColumnsLeft:0,outsideClickDeselects:!0,enterBeginsEditing:!0,enterMoves:{row:1,col:0},tabMoves:{row:0,col:1},autoWrapRow:!1,autoWrapCol:!1,copyRowsLimit:1e3,copyColsLimit:1e3,pasteMode:"overwrite",currentRowClassName:void 0,currentColClassName:void 0,stretchH:"none",isEmptyRow:function(a){for(var b,c=0,d=this.countCols();d>c;c++)if(b=this.getDataAtCell(a,c),""!==b&&null!==b&&"undefined"!=typeof b)return!1;return!0},isEmptyCol:function(a){for(var b,c=0,d=this.countRows();d>c;c++)if(b=this.getDataAtCell(c,a),""!==b&&null!==b&&"undefined"!=typeof b)return!1;return!0},observeDOMVisibility:!0,allowInvalid:!0,invalidCellClassName:"htInvalid",placeholder:!1,placeholderCellClassName:"htPlaceholder",readOnlyCellClassName:"htDimmed",commentedCellClassName:"htCommentCell",fragmentSelection:!1,readOnly:!1,type:"text",copyable:!0,debug:!1,wordWrap:!0,noWordWrapClassName:"htNoWrap",contextMenu:void 0,undo:void 0,columnSorting:void 0,manualColumnMove:void 0,manualColumnResize:void 0,manualRowMove:void 0,manualRowResize:void 0,manualColumnFreeze:void 0,viewportRowRenderingOffset:10,viewportColumnRenderingOffset:10,groups:void 0},b.DefaultSettings=G,function(a){function b(){function a(a){return null!==a&&!c(a)&&("string"==typeof a||"number"==typeof a)}function b(a){return null!==a&&("object"==typeof a||"function"==typeof a)}function c(a){return a!==a}var d={arrayMap:[],weakMap:new WeakMap};return{get:function(c){return a(c)?d.arrayMap[c]:b(c)?d.weakMap.get(c):void 0},set:function(c,e){if(a(c))d.arrayMap[c]=e;else{if(!b(c))throw new Error("Invalid key type");d.weakMap.set(c,e)}},"delete":function(c){a(c)?delete d.arrayMap[c]:b(c)&&d.weakMap["delete"](c)}}}a.MultiMap||(a.MultiMap=b)}(a),!a.Handsontable)var b={};if(b.Dom={},b.Dom.enableImmediatePropagation=function(a){null!=a&&null==a.isImmediatePropagationEnabled&&(a.stopImmediatePropagation=function(){this.isImmediatePropagationEnabled=!1,this.cancelBubble=!0},a.isImmediatePropagationEnabled=!0,a.isImmediatePropagationStopped=function(){return!this.isImmediatePropagationEnabled})},b.Dom.closest=function(a,b,c){for(;null!=a&&a!==c;){if(1===a.nodeType&&b.indexOf(a.nodeName)>-1)return a;a=a.parentNode}return null},b.Dom.isChildOf=function(a,b){var c=a.parentNode,d=[];for("string"==typeof b?d=Array.prototype.slice.call(document.querySelectorAll(b),0):d.push(b);null!=c;){if(d.indexOf(c)>-1)return!0;c=c.parentNode}return!1},b.Dom.index=function(a){var b=0;if(a.previousSibling)for(;a=a.previousSibling;)++b;return b},document.documentElement.classList?(b.Dom.hasClass=function(a,b){return a.classList.contains(b)},b.Dom.addClass=function(a,b){b&&a.classList.add(b)},b.Dom.removeClass=function(a,b){a.classList.remove(b)}):(b.Dom.hasClass=function(a,b){return a.className.match(new RegExp("(\\s|^)"+b+"(\\s|$)"))},b.Dom.addClass=function(a,b){""===a.className?a.className=b:this.hasClass(a,b)||(a.className+=" "+b)},b.Dom.removeClass=function(a,b){if(this.hasClass(a,b)){var c=new RegExp("(\\s|^)"+b+"(\\s|$)");a.className=a.className.replace(c," ").trim()}}),b.Dom.removeTextNodes=function(a,b){if(3===a.nodeType)b.removeChild(a);else if(["TABLE","THEAD","TBODY","TFOOT","TR"].indexOf(a.nodeName)>-1)for(var c=a.childNodes,d=c.length-1;d>=0;d--)this.removeTextNodes(c[d],a)},b.Dom.empty=function(a){for(var b;b=a.lastChild;)a.removeChild(b)},b.Dom.HTML_CHARACTERS=/(<(.*)>|&(.*);)/,b.Dom.fastInnerHTML=function(a,b){this.HTML_CHARACTERS.test(b)?a.innerHTML=b:this.fastInnerText(a,b)},b.Dom.fastInnerText=document.createTextNode("test").textContent?function(a,b){var c=a.firstChild;c&&3===c.nodeType&&null===c.nextSibling?c.textContent=b:(this.empty(a),a.appendChild(document.createTextNode(b)))}:function(a,b){var c=a.firstChild;c&&3===c.nodeType&&null===c.nextSibling?c.data=b:(this.empty(a),a.appendChild(document.createTextNode(b)))},b.Dom.isVisible=function(a){for(var c=a;c!==document.documentElement;){if(null===c)return!1;if(11===c.nodeType){if(c.host){if(c.host.impl)return b.Dom.isVisible(c.host.impl);if(c.host)return b.Dom.isVisible(c.host);throw new Error("Lost in Web Components world")}return!1}if("none"===c.style.display)return!1;c=c.parentNode}return!0},b.Dom.offset=function(b){var c,d,e,f,g;if(f=document.documentElement,this.hasCaptionProblem()&&b.firstChild&&"CAPTION"===b.firstChild.nodeName)return g=b.getBoundingClientRect(),{top:g.top+(a.pageYOffset||f.scrollTop)-(f.clientTop||0),left:g.left+(a.pageXOffset||f.scrollLeft)-(f.clientLeft||0)};for(c=b.offsetLeft,d=b.offsetTop,e=b;(b=b.offsetParent)&&b!==document.body;)c+=b.offsetLeft,d+=b.offsetTop,e=b;return e&&"fixed"===e.style.position&&(c+=a.pageXOffset||f.scrollLeft,d+=a.pageYOffset||f.scrollTop),{left:c,top:d}},b.Dom.getWindowScrollTop=function(){var b=a.scrollY;return void 0==b&&(b=document.documentElement.scrollTop),b},b.Dom.getWindowScrollLeft=function(){var b=a.scrollX;return void 0==b&&(b=document.documentElement.scrollLeft),b},b.Dom.getScrollTop=function(c){return c===a?b.Dom.getWindowScrollTop(c):c.scrollTop},b.Dom.getScrollLeft=function(c){return c===a?b.Dom.getWindowScrollLeft(c):c.scrollLeft},b.Dom.getScrollableElement=function(b){for(var c,d,e,f=b.parentNode,g=["auto","scroll"];f&&f.style;){if(c=f.style.overflow,d=f.style.overflowX,e=f.style.overflowY,"scroll"==c||"scroll"==d||"scroll"==e)return f;if(f.clientHeight";var c=document.createElement("CAPTION");c.innerHTML="c
c
c
c",c.style.padding=0,c.style.margin=0,a.insertBefore(c,b),document.body.appendChild(a),d=a.offsetHeight<2*a.lastChild.offsetHeight,document.body.removeChild(a)}function c(){var a=document.createElement("p");a.style.width="100%",a.style.height="200px";var b=document.createElement("div");b.style.position="absolute",b.style.top="0px",b.style.left="0px",b.style.visibility="hidden",b.style.width="200px",b.style.height="150px",b.style.overflow="hidden",b.appendChild(a),(document.body||document.documentElement).appendChild(b);var c=a.offsetWidth;b.style.overflow="scroll";var d=a.offsetWidth;return c==d&&(d=b.clientWidth),(document.body||document.documentElement).removeChild(b),c-d}var d;b.Dom.hasCaptionProblem=function(){return void 0===d&&a(),d},b.Dom.getCaretPosition=function(a){if(a.selectionStart)return a.selectionStart;if(document.selection){a.focus();var b=document.selection.createRange();if(null==b)return 0;var c=a.createTextRange(),d=c.duplicate();return c.moveToBookmark(b.getBookmark()),d.setEndPoint("EndToStart",c),d.text.length}return 0},b.Dom.getSelectionEndPosition=function(a){if(a.selectionEnd)return a.selectionEnd;if(document.selection){var b=document.selection.createRange();if(null==b)return 0;var c=a.createTextRange();return c.text.indexOf(b.text)+b.text.length}},b.Dom.setCaretPosition=function(a,b,c){if(void 0===c&&(c=b),a.setSelectionRange)a.focus(),a.setSelectionRange(b,c);else if(a.createTextRange){var d=a.createTextRange();d.collapse(!0),d.moveEnd("character",c),d.moveStart("character",b),d.select()}};var e;b.Dom.getScrollbarWidth=function(){return void 0===e&&(e=c()),e};var f=!document.createTextNode("test").textContent;b.Dom.isIE8=function(){return f};var g=!!document.documentMode;b.Dom.isIE9=function(){return g};var h=/Safari/.test(navigator.userAgent)&&/Apple Computer/.test(navigator.vendor);b.Dom.isSafari=function(){return h},b.Dom.setOverlayPosition=function(a,b,c){f||g?(a.style.top=c,a.style.left=b):h?a.style["-webkit-transform"]="translate3d("+b+","+c+",0)":a.style.transform="translate3d("+b+","+c+",0)"},b.Dom.getCssTransform=function(a){var b;return a.style.transform&&""!=(b=a.style.transform)?["transform",b]:a.style["-webkit-transform"]&&""!=(b=a.style["-webkit-transform"])?["-webkit-transform",b]:-1},b.Dom.resetCssTransform=function(a){a.transform&&""!=a.transform?a.transform="":a["-webkit-transform"]&&""!=a["-webkit-transform"]&&(a["-webkit-transform"]="")}}(),!a.Handsontable)var b={};b.countEventManagerListeners=0,b.eventManager=function(c){var d,e,f,g;if(!c)throw new Error("instance not defined");return c.eventListeners||(c.eventListeners=[]),d=function(d,f,g){var h;return h=function(a){void 0==a.target&&void 0!=a.srcElement&&(a.definePoperty?a.definePoperty("target",{value:a.srcElement}):a.target=a.srcElement),void 0==a.preventDefault&&(a.definePoperty?a.definePoperty("preventDefault",{value:function(){this.returnValue=!1}}):a.preventDefault=function(){this.returnValue=!1}),g.call(this,a)},c.eventListeners.push({element:d,event:f,callback:g,callbackProxy:h}),a.addEventListener?d.addEventListener(f,h,!1):d.attachEvent("on"+f,h),b.countEventManagerListeners++,function(){e(d,f,g)}},e=function(a,d,e){for(var f,g=c.eventListeners.length;g--;)if(f=c.eventListeners[g],f.event==d&&f.element==a){if(e&&e!=f.callback)continue;c.eventListeners.splice(g,1),f.element.removeEventListener?f.element.removeEventListener(f.event,f.callbackProxy,!1):f.element.detachEvent("on"+f.event,f.callbackProxy),b.countEventManagerListeners--}},f=function(){for(var a,b=c.eventListeners.length;b--;)a=c.eventListeners[b],a&&e(a.element,a.event,a.callback)},g=function(b,c){var d,e;d={bubbles:!0,cancelable:"mousemove"!==c,view:a,detail:0,screenX:0,screenY:0,clientX:1,clientY:1,ctrlKey:!1,altKey:!1,shiftKey:!1,metaKey:!1,button:0,relatedTarget:void 0},document.createEvent?(e=document.createEvent("MouseEvents"),e.initMouseEvent(c,d.bubbles,d.cancelable,d.view,d.detail,d.screenX,d.screenY,d.clientX,d.clientY,d.ctrlKey,d.altKey,d.shiftKey,d.metaKey,d.button,d.relatedTarget||document.body.parentNode)):e=document.createEventObject(),b.dispatchEvent?b.dispatchEvent(e):b.fireEvent("on"+c,e)},{addEventListener:d,removeEventListener:e,clear:f,fireEvent:g}},b.TableView=function(c){var d=this;this.eventManager=b.eventManager(c),this.instance=c,this.settings=c.getSettings();var e=c.rootElement.getAttribute("style");e&&c.rootElement.setAttribute("data-originalstyle",e),b.Dom.addClass(c.rootElement,"handsontable");var f=document.createElement("TABLE");f.className="htCore",this.THEAD=document.createElement("THEAD"),f.appendChild(this.THEAD),this.TBODY=document.createElement("TBODY"),f.appendChild(this.TBODY),c.table=f,c.container.insertBefore(f,c.container.firstChild),this.eventManager.addEventListener(c.rootElement,"mousedown",function(b){d.isTextSelectionAllowed(b.target)||(h(),b.preventDefault(),a.focus())}),this.eventManager.addEventListener(document.documentElement,"keyup",function(a){c.selection.isInProgress()&&!a.shiftKey&&c.selection.finish()});var g;this.isMouseDown=function(){return g},this.eventManager.addEventListener(document.documentElement,"mouseup",function(a){c.selection.isInProgress()&&1===a.which&&c.selection.finish(),g=!1,b.helper.isOutsideInput(document.activeElement)&&c.unlisten()}),this.eventManager.addEventListener(document.documentElement,"mousedown",function(a){var b=a.target;if(!g){if(b!==d.wt.wtTable.spreader)for(;b!==document.documentElement;){if(null===b)return;if(b===c.rootElement)return;b=b.parentNode}d.settings.outsideClickDeselects?c.deselectCell():c.destroyEditor()}}),this.eventManager.addEventListener(f,"selectstart",function(a){d.settings.fragmentSelection||a.preventDefault()});var h=function(){a.getSelection?a.getSelection().empty?a.getSelection().empty():a.getSelection().removeAllRanges&&a.getSelection().removeAllRanges():document.selection&&document.selection.empty()},i=[new z({className:"current",border:{width:2,color:"#5292F7",cornerVisible:function(){return d.settings.fillHandle&&!d.isCellEdited()&&!c.selection.isMultiple()},multipleSelectionHandlesVisible:function(){return!d.isCellEdited()&&!c.selection.isMultiple()}}}),new z({className:"area",border:{width:1,color:"#89AFF9",cornerVisible:function(){return d.settings.fillHandle&&!d.isCellEdited()&&c.selection.isMultiple()},multipleSelectionHandlesVisible:function(){return!d.isCellEdited()&&c.selection.isMultiple()}}}),new z({className:"highlight",highlightRowClassName:d.settings.currentRowClassName,highlightColumnClassName:d.settings.currentColClassName}),new z({className:"fill",border:{width:1,color:"red"}})];i.current=i[0],i.area=i[1],i.highlight=i[2],i.fill=i[3];var j={debug:function(){return d.settings.debug},table:f,stretchH:this.settings.stretchH,data:c.getDataAtCell,totalRows:c.countRows,totalColumns:c.countCols,fixedColumnsLeft:function(){return d.settings.fixedColumnsLeft},fixedRowsTop:function(){return d.settings.fixedRowsTop},renderAllRows:d.settings.renderAllRows,rowHeaders:function(){var a=[];return c.hasRowHeaders()&&a.push(function(a,b){d.appendRowHeader(a,b)}),b.hooks.run(c,"afterGetRowHeaderRenderers",a),a},columnHeaders:function(){var a=[];return c.hasColHeaders()&&a.push(function(a,b){d.appendColHeader(a,b)}),b.hooks.run(c,"afterGetColumnHeaderRenderers",a),a},columnWidth:c.getColWidth,rowHeight:c.getRowHeight,cellRenderer:function(a,c,e){var f=d.instance.colToProp(c),g=d.instance.getCellMeta(a,c),h=d.instance.getCellRenderer(g),i=d.instance.getDataAtRowProp(a,f); +h(d.instance,e,a,c,f,i,g),b.hooks.run(d.instance,"afterRenderer",e,a,c,f,i,g)},selections:i,hideBorderOnMouseDownOver:function(){return d.settings.fragmentSelection},onCellMouseDown:function(a,e,f,h){c.listen(),d.activeWt=h,g=!0,b.hooks.run(c,"beforeOnCellMouseDown",a,e,f),b.Dom.enableImmediatePropagation(a),a.isImmediatePropagationStopped()||(2===a.button&&c.selection.inInSelection(e)||(a.shiftKey?e.row>=0&&e.col>=0&&c.selection.setRangeEnd(e):e.row<0||e.col<0?(e.row<0&&(c.selectCell(0,e.col,c.countRows()-1,e.col),c.selection.setSelectedHeaders(!1,!0)),e.col<0&&(c.selectCell(e.row,0,e.row,c.countCols()-1),c.selection.setSelectedHeaders(!0,!1))):c.selection.setRangeStart(e)),b.hooks.run(c,"afterOnCellMouseDown",a,e,f),d.activeWt=d.wt)},onCellMouseOver:function(a,e,f,h){d.activeWt=h,e.row>=0&&e.col>=0?g&&c.selection.setRangeEnd(e):g&&(e.row<0&&(c.selection.setRangeEnd(new l(c.countRows()-1,e.col)),c.selection.setSelectedHeaders(!1,!0)),e.col<0&&(c.selection.setRangeEnd(new l(e.row,c.countCols()-1)),c.selection.setSelectedHeaders(!0,!1))),b.hooks.run(c,"afterOnCellMouseOver",a,e,f),d.activeWt=d.wt},onCellCornerMouseDown:function(a){a.preventDefault(),b.hooks.run(c,"afterOnCellCornerMouseDown",a)},beforeDraw:function(a){d.beforeRender(a)},onDraw:function(a){d.onDraw(a)},onScrollVertically:function(){c.runHooks("afterScrollVertically")},onScrollHorizontally:function(){c.runHooks("afterScrollHorizontally")},onBeforeDrawBorders:function(a,b){c.runHooks("beforeDrawBorders",a,b)},onBeforeTouchScroll:function(){c.runHooks("beforeTouchScroll")},onAfterMomentumScroll:function(){c.runHooks("afterMomentumScroll")},viewportRowCalculatorOverride:function(a){d.settings.viewportRowRenderingOffset&&(a.startRow=Math.max(a.startRow-d.settings.viewportRowRenderingOffset,0),a.endRow=Math.min(a.endRow+d.settings.viewportRowRenderingOffset,c.countRows()-1)),c.runHooks("afterViewportRowCalculatorOverride",a)},viewportColumnCalculatorOverride:function(a){d.settings.viewportColumnRenderingOffset&&(a.startColumn=Math.max(a.startColumn-d.settings.viewportColumnRenderingOffset,0),a.endColumn=Math.min(a.endColumn+d.settings.viewportColumnRenderingOffset,c.countCols()-1)),c.runHooks("afterViewportColumnCalculatorOverride",a)}};b.hooks.run(c,"beforeInitWalkontable",j),this.wt=new p(j),this.activeWt=this.wt,this.eventManager.addEventListener(d.wt.wtTable.spreader,"mousedown",function(a){a.target===d.wt.wtTable.spreader&&3===a.which&&b.helper.stopPropagation(a)}),this.eventManager.addEventListener(d.wt.wtTable.spreader,"contextmenu",function(a){a.target===d.wt.wtTable.spreader&&3===a.which&&b.helper.stopPropagation(a)}),this.eventManager.addEventListener(document.documentElement,"click",function(){d.settings.observeDOMVisibility&&d.wt.drawInterrupted&&(d.instance.forceFullRender=!0,d.render())})},b.TableView.prototype.isTextSelectionAllowed=function(a){return b.helper.isInput(a)?!0:this.settings.fragmentSelection&&b.Dom.isChildOf(a,this.TBODY)?!0:!1},b.TableView.prototype.isCellEdited=function(){var a=this.instance.getActiveEditor();return a&&a.isOpened()},b.TableView.prototype.beforeRender=function(a){a&&b.hooks.run(this.instance,"beforeRender",this.instance.forceFullRender)},b.TableView.prototype.onDraw=function(a){a&&b.hooks.run(this.instance,"afterRender",this.instance.forceFullRender)},b.TableView.prototype.render=function(){this.wt.draw(!this.instance.forceFullRender),this.instance.forceFullRender=!1},b.TableView.prototype.getCellAtCoords=function(a,b){var c=this.wt.getCell(a,b);return 0>c?null:c},b.TableView.prototype.scrollViewport=function(a){this.wt.scrollViewport(a)},b.TableView.prototype.appendRowHeader=function(a,c){var d=document.createElement("DIV"),e=document.createElement("SPAN");d.className="relative",e.className="rowHeader",a>-1?b.Dom.fastInnerHTML(e,this.instance.getRowHeader(a)):b.Dom.fastInnerText(e,String.fromCharCode(160)),d.appendChild(e),b.Dom.empty(c),c.appendChild(d),b.hooks.run(this.instance,"afterGetRowHeader",a,c)},b.TableView.prototype.appendColHeader=function(a,c){var d=document.createElement("DIV"),e=document.createElement("SPAN");d.className="relative",e.className="colHeader",a>-1?b.Dom.fastInnerHTML(e,this.instance.getColHeader(a)):b.Dom.fastInnerText(e,String.fromCharCode(160)),d.appendChild(e),b.Dom.empty(c),c.appendChild(d),b.hooks.run(this.instance,"afterGetColHeader",a,c)},b.TableView.prototype.maximumVisibleElementWidth=function(a){var b=this.wt.wtViewport.getWorkspaceWidth(),c=b-a;return c>0?c:0},b.TableView.prototype.maximumVisibleElementHeight=function(a){var b=this.wt.wtViewport.getWorkspaceHeight(),c=b-a;return c>0?c:0},b.TableView.prototype.mainViewIsActive=function(){return this.wt===this.activeWt},b.TableView.prototype.destroy=function(){this.wt.destroy(),this.eventManager.clear()},function(a){function b(a){var b,c;c={},b=a,this.getInstance=function(a){return a.guid in c||(c[a.guid]=new b(a)),c[a.guid]}}var c={},d=new WeakMap;a.editors={registerEditor:function(a,e){var f=new b(e);"string"==typeof a&&(c[a]=f),d.set(e,f)},getEditor:function(a,b){var e;if("function"==typeof a)d.get(a)||this.registerEditor(null,a),e=d.get(a);else{if("string"!=typeof a)throw Error('Only strings and functions can be passed as "editor" parameter ');e=c[a]}if(!e)throw Error('No editor registered under name "'+a+'"');return e.getInstance(b)}}}(b),function(a){a.EditorManager=function(b,c,d){var e,f=this,g=a.helper.keyCode,h=!1,i=a.eventManager(b),j=function(){function j(i){if(b.isListening()&&(a.hooks.run(b,"beforeKeyDown",i),!h&&(a.Dom.enableImmediatePropagation(i),!i.isImmediatePropagationStopped()&&(c.lastKeyCode=i.keyCode,d.isSelected())))){var j=(i.ctrlKey||i.metaKey)&&!i.altKey;if(!(e.isWaiting()||a.helper.isMetaKey(i.keyCode)||j||f.isEditorOpened()))return void f.openEditor("");var k=i.shiftKey?d.setRangeEnd:d.setRangeStart;switch(i.keyCode){case g.A:j&&(d.selectAll(),i.preventDefault(),a.helper.stopPropagation(i));break;case g.ARROW_UP:f.isEditorOpened()&&!e.isWaiting()&&f.closeEditorAndSaveChanges(j),n(i.shiftKey),i.preventDefault(),a.helper.stopPropagation(i);break;case g.ARROW_DOWN:f.isEditorOpened()&&!e.isWaiting()&&f.closeEditorAndSaveChanges(j),o(i.shiftKey),i.preventDefault(),a.helper.stopPropagation(i);break;case g.ARROW_RIGHT:f.isEditorOpened()&&!e.isWaiting()&&f.closeEditorAndSaveChanges(j),p(i.shiftKey),i.preventDefault(),a.helper.stopPropagation(i);break;case g.ARROW_LEFT:f.isEditorOpened()&&!e.isWaiting()&&f.closeEditorAndSaveChanges(j),q(i.shiftKey),i.preventDefault(),a.helper.stopPropagation(i);break;case g.TAB:var r="function"==typeof c.settings.tabMoves?c.settings.tabMoves(i):c.settings.tabMoves;i.shiftKey?d.transformStart(-r.row,-r.col):d.transformStart(r.row,r.col,!0),i.preventDefault(),a.helper.stopPropagation(i);break;case g.BACKSPACE:case g.DELETE:d.empty(i),f.prepareEditor(),i.preventDefault();break;case g.F2:f.openEditor(),i.preventDefault();break;case g.ENTER:f.isEditorOpened()?(e.state!==a.EditorState.WAITING&&f.closeEditorAndSaveChanges(j),m(i.shiftKey)):b.getSettings().enterBeginsEditing?f.openEditor():m(i.shiftKey),i.preventDefault(),i.stopImmediatePropagation();break;case g.ESCAPE:f.isEditorOpened()&&f.closeEditorAndRestoreOriginalValue(j),i.preventDefault();break;case g.HOME:k(i.ctrlKey||i.metaKey?new l(0,c.selRange.from.col):new l(c.selRange.from.row,0)),i.preventDefault(),a.helper.stopPropagation(i);break;case g.END:k(i.ctrlKey||i.metaKey?new l(b.countRows()-1,c.selRange.from.col):new l(c.selRange.from.row,b.countCols()-1)),i.preventDefault(),a.helper.stopPropagation(i);break;case g.PAGE_UP:d.transformStart(-b.countVisibleRows(),0),i.preventDefault(),a.helper.stopPropagation(i);break;case g.PAGE_DOWN:d.transformStart(b.countVisibleRows(),0),i.preventDefault(),a.helper.stopPropagation(i)}}}function k(a,b,c){"TD"==c.nodeName&&f.openEditor()}function m(a){var b="function"==typeof c.settings.enterMoves?c.settings.enterMoves(event):c.settings.enterMoves;a?d.transformStart(-b.row,-b.col):d.transformStart(b.row,b.col,!0)}function n(a){a?d.transformEnd(-1,0):d.transformStart(-1,0)}function o(a){a?d.transformEnd(1,0):d.transformStart(1,0)}function p(a){a?d.transformEnd(0,1):d.transformStart(0,1)}function q(a){a?d.transformEnd(0,-1):d.transformStart(0,-1)}b.addHook("afterDocumentKeyDown",function(a){j(a)}),i.addEventListener(document,"keydown",function(a){b.runHooks("afterDocumentKeyDown",a)}),b.view.wt.update("onCellDblClick",k),b.addHook("afterDestroy",function(){h=!0})};this.destroyEditor=function(a){this.closeEditor(a)},this.getActiveEditor=function(){return e},this.prepareEditor=function(){if(e&&e.isWaiting())return void this.closeEditor(!1,!1,function(a){a&&f.prepareEditor()});var d=c.selRange.highlight.row,g=c.selRange.highlight.col,h=b.colToProp(g),i=b.getCell(d,g),j=b.getDataAtCell(d,g),k=b.getCellMeta(d,g),l=b.getCellEditor(k);e=a.editors.getEditor(l,b),e.prepare(d,g,h,i,j,k)},this.isEditorOpened=function(){return e.isOpened()},this.openEditor=function(a){e.cellProperties.readOnly||e.beginEditing(a)},this.closeEditor=function(a,b,c){e?e.finishEditing(a,b,c):c&&c(!1)},this.closeEditorAndSaveChanges=function(a){return this.closeEditor(!1,a)},this.closeEditorAndRestoreOriginalValue=function(a){return this.closeEditor(!0,a)},j()}}(b),function(a){var b={};a.renderers={registerRenderer:function(a,c){b[a]=c},getRenderer:function(a){if("function"==typeof a)return a;if("string"!=typeof a)throw Error('Only strings and functions can be passed as "renderer" parameter ');if(!(a in b))throw Error('No editor registered under name "'+a+'"');return b[a]}}}(b),b.helper={},b.helper.isPrintableChar=function(a){return 32==a||a>=48&&57>=a||a>=96&&111>=a||a>=186&&192>=a||a>=219&&222>=a||a>=226||a>=65&&90>=a},b.helper.isMetaKey=function(a){var c=b.helper.keyCode,d=[c.ARROW_DOWN,c.ARROW_UP,c.ARROW_LEFT,c.ARROW_RIGHT,c.HOME,c.END,c.DELETE,c.BACKSPACE,c.F1,c.F2,c.F3,c.F4,c.F5,c.F6,c.F7,c.F8,c.F9,c.F10,c.F11,c.F12,c.TAB,c.PAGE_DOWN,c.PAGE_UP,c.ENTER,c.ESCAPE,c.SHIFT,c.CAPS_LOCK,c.ALT];return-1!=d.indexOf(a)},b.helper.isCtrlKey=function(a){var c=b.helper.keyCode;return-1!=[c.CONTROL_LEFT,224,c.COMMAND_LEFT,c.COMMAND_RIGHT].indexOf(a)},b.helper.stringify=function(a){switch(typeof a){case"string":case"number":return a+"";case"object":return null===a?"":a.toString();case"undefined":return"";default:return a.toString()}},b.helper.spreadsheetColumnLabel=function(a){for(var b,c=a+1,d="";c>0;)b=(c-1)%26,d=String.fromCharCode(65+b)+d,c=parseInt((c-b)/26,10);return d},b.helper.createSpreadsheetData=function(a,c){a="number"==typeof a?a:100,c="number"==typeof c?c:4;var d,e,f=[];for(d=0;a>d;d++){var g=[];for(e=0;c>e;e++)g.push(b.helper.spreadsheetColumnLabel(e)+(d+1));f.push(g)}return f},b.helper.createSpreadsheetObjectData=function(a,c){a="number"==typeof a?a:100,c="number"==typeof c?c:4;var d,e,f=[];for(d=0;a>d;d++){var g={};for(e=0;c>e;e++)g["prop"+e]=b.helper.spreadsheetColumnLabel(e)+(d+1);f.push(g)}return f},b.helper.isNumeric=function(a){var b=typeof a;return"number"==b?!isNaN(a)&&isFinite(a):"string"==b?a.length?1==a.length?/\d/.test(a):/^\s*[+-]?\s*(?:(?:\d+(?:\.\d+)?(?:e[+-]?\d+)?)|(?:0x[a-f\d]+))\s*$/i.test(a):!1:"object"==b?!(!a||"number"!=typeof a.valueOf()||a instanceof Date):!1},b.helper.randomString=function(){return s()},b.helper.inherit=function(a,b){return b.prototype.constructor=b,a.prototype=new b,a.prototype.constructor=a,a},b.helper.extend=function(a,b){for(var c in b)b.hasOwnProperty(c)&&(a[c]=b[c])},b.helper.deepExtend=function(a,c){for(var d in c)c.hasOwnProperty(d)&&(c[d]&&"object"==typeof c[d]?(a[d]||(a[d]=Array.isArray(c[d])?[]:{}),b.helper.deepExtend(a[d],c[d])):a[d]=c[d])},b.helper.deepClone=function(a){return"object"==typeof a?JSON.parse(JSON.stringify(a)):a},b.helper.getPrototypeOf=function(a){var b;if("object"==typeof a.__proto__)b=a.__proto__;else{var c,d=a.constructor;"function"==typeof a.constructor&&(c=d,delete a.constructor&&(d=a.constructor,a.constructor=c)),b=d?d.prototype:null}return b},b.helper.columnFactory=function(a,c){function d(){}b.helper.inherit(d,a);for(var e=0,f=c.length;f>e;e++)d.prototype[c[e]]=void 0;return d},b.helper.translateRowsToColumns=function(a){var b,c,d,e,f=[],g=0;for(b=0,c=a.length;c>b;b++)for(d=0,e=a[b].length;e>d;d++)d==g&&(f.push([]),g++),f[d].push(a[b][d]);return f},b.helper.to2dArray=function(a){for(var b=0,c=a.length;c>b;)a[b]=[a[b]],b++},b.helper.extendArray=function(a,b){for(var c=0,d=b.length;d>c;)a.push(b[c]),c++},b.helper.isInput=function(a){var b=["INPUT","SELECT","TEXTAREA"];return b.indexOf(a.nodeName)>-1},b.helper.isOutsideInput=function(a){return b.helper.isInput(a)&&-1==a.className.indexOf("handsontableInput")},b.helper.keyCode={MOUSE_LEFT:1,MOUSE_RIGHT:3,MOUSE_MIDDLE:2,BACKSPACE:8,COMMA:188,INSERT:45,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},b.helper.isObject=function(a){return"[object Object]"==Object.prototype.toString.call(a)},b.helper.pivot=function(a){var b=[];if(!a||0===a.length||!a[0]||0===a[0].length)return b;for(var c=a.length,d=a[0].length,e=0;c>e;e++)for(var f=0;d>f;f++)b[f]||(b[f]=[]),b[f][e]=a[e][f];return b},b.helper.proxy=function(a,b){return function(){return a.apply(b,arguments)}},b.helper.cellMethodLookupFactory=function(a,c){function d(a){var c=b.cellTypes[a];if("undefined"==typeof c)throw new Error('You declared cell type "'+a+'" 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 c}return c="undefined"==typeof c?!0:c,function(e,f){return function g(e){if(e){if(e.hasOwnProperty(a)&&void 0!==e[a])return e[a];if(e.hasOwnProperty("type")&&e.type){var f;if("string"!=typeof e.type)throw new Error("Cell type must be a string ");if(f=d(e.type),f.hasOwnProperty(a))return f[a];if(c)return}return g(b.helper.getPrototypeOf(e))}}("number"==typeof e?this.getCellMeta(e,f):e)}},b.helper.isMobileBrowser=function(a){return a||(a=navigator.userAgent),/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(a)},b.helper.isTouchSupported=function(){return"ontouchstart"in a},b.helper.stopPropagation=function(a){"function"==typeof a.stopPropagation?a.stopPropagation():a.cancelBubble=!0},b.helper.pageX=function(a){if(a.pageX)return a.pageX;var c=b.Dom.getWindowScrollLeft(),d=a.clientX+c;return d},b.helper.pageY=function(a){if(a.pageY)return a.pageY;var c=b.Dom.getWindowScrollTop(),d=a.clientY+c;return d},function(a){a.DataMap=function(a,b,c){this.instance=a,this.priv=b,this.GridSettings=c,this.dataSource=this.instance.getSettings().data,this.duckSchema=this.dataSource[0]?this.recursiveDuckSchema(this.dataSource[0]):{},this.createMap()},a.DataMap.prototype.DESTINATION_RENDERER=1,a.DataMap.prototype.DESTINATION_CLIPBOARD_GENERATOR=2,a.DataMap.prototype.recursiveDuckSchema=function(a){var b;if(Array.isArray(a))b=[];else{b={};for(var c in a)a.hasOwnProperty(c)&&(b[c]="object"!=typeof a[c]||Array.isArray(a[c])?null:this.recursiveDuckSchema(a[c]))}return b},a.DataMap.prototype.recursiveDuckColumns=function(a,b,c){var d,e;if("undefined"==typeof b&&(b=0,c=""),"object"==typeof a&&!Array.isArray(a))for(e in a)a.hasOwnProperty(e)&&(null===a[e]?(d=c+e,this.colToPropCache.push(d),this.propToColCache.set(d,b),b++):b=this.recursiveDuckColumns(a[e],b,e+"."));return b},a.DataMap.prototype.createMap=function(){var a,b,c=this.getSchema();if("undefined"==typeof c)throw new Error("trying to create `columns` definition but you didnt' provide `schema` nor `data`");this.colToPropCache=[],this.propToColCache=new MultiMap;var d=this.instance.getSettings().columns;if(d)for(a=0,b=d.length;b>a;a++)"undefined"!=typeof d[a].data&&(this.colToPropCache[a]=d[a].data,this.propToColCache.set(d[a].data,a));else this.recursiveDuckColumns(c)},a.DataMap.prototype.colToProp=function(b){return b=a.hooks.run(this.instance,"modifyCol",b),this.colToPropCache&&"undefined"!=typeof this.colToPropCache[b]?this.colToPropCache[b]:b},a.DataMap.prototype.propToCol=function(b){var c;return c="undefined"!=typeof this.propToColCache.get(b)?this.propToColCache.get(b):b,c=a.hooks.run(this.instance,"modifyCol",c)},a.DataMap.prototype.getSchema=function(){var a=this.instance.getSettings().dataSchema;return a?"function"==typeof a?a():a:this.duckSchema},a.DataMap.prototype.createRow=function(b,c,d){var e,f,g=this.instance.countCols(),h=0;c||(c=1),("number"!=typeof b||b>=this.instance.countRows())&&(b=this.instance.countRows()),f=b;for(var i=this.instance.getSettings().maxRows;c>h&&this.instance.countRows()j;j++)e.push(null)}else"function"===this.instance.dataType?e=this.instance.getSettings().dataSchema(b):(e={},a.helper.deepExtend(e,this.getSchema()));b===this.instance.countRows()?this.dataSource.push(e):this.dataSource.splice(b,0,e),h++,f++}return a.hooks.run(this.instance,"afterCreateRow",b,h,d),this.instance.forceFullRender=!0,h},a.DataMap.prototype.createCol=function(b,c,d){if("object"===this.instance.dataType||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 e,f,g=this.instance.countRows(),h=this.dataSource,i=0;c||(c=1),f=b;for(var j=this.instance.getSettings().maxCols;c>i&&this.instance.countCols()=this.instance.countCols()){for(var k=0;g>k;k++)"undefined"==typeof h[k]&&(h[k]=[]),h[k].push(null);this.priv.columnSettings.push(e)}else{for(var k=0;g>k;k++)h[k].splice(f,0,null);this.priv.columnSettings.splice(f,0,e)}i++,f++}return a.hooks.run(this.instance,"afterCreateCol",b,i,d),this.instance.forceFullRender=!0,i},a.DataMap.prototype.removeRow=function(b,c){c||(c=1),"number"!=typeof b&&(b=-c),b=(this.instance.countRows()+b)%this.instance.countRows();var d=this.physicalRowsToLogical(b,c),e=a.hooks.run(this.instance,"beforeRemoveRow",b,c);if(e!==!1){var f=this.dataSource,g=f.filter(function(a,b){return-1==d.indexOf(b)});f.length=0,Array.prototype.push.apply(f,g),a.hooks.run(this.instance,"afterRemoveRow",b,c),this.instance.forceFullRender=!0}},a.DataMap.prototype.removeCol=function(b,c){if("object"===this.instance.dataType||this.instance.getSettings().columns)throw new Error("cannot remove column with object data source or columns option specified");c||(c=1),"number"!=typeof b&&(b=-c),b=(this.instance.countCols()+b)%this.instance.countCols();var d=a.hooks.run(this.instance,"beforeRemoveCol",b,c);if(d!==!1){for(var e=this.dataSource,f=0,g=this.instance.countRows();g>f;f++)e[f].splice(b,c);this.priv.columnSettings.splice(b,c),a.hooks.run(this.instance,"afterRemoveCol",b,c),this.instance.forceFullRender=!0}},a.DataMap.prototype.spliceCol=function(b,c,d){var e=4<=arguments.length?[].slice.call(arguments,3):[],f=this.instance.getDataAtCol(b),g=f.slice(c,c+d),h=f.slice(c+d);a.helper.extendArray(e,h);for(var i=0;d>i;)e.push(null),i++;return a.helper.to2dArray(e),this.instance.populateFromArray(c,b,e,null,null,"spliceCol"),g},a.DataMap.prototype.spliceRow=function(b,c,d){var e=4<=arguments.length?[].slice.call(arguments,3):[],f=this.instance.getSourceDataAtRow(b),g=f.slice(c,c+d),h=f.slice(c+d);a.helper.extendArray(e,h);for(var i=0;d>i;)e.push(null),i++;return this.instance.populateFromArray(b,c,[e],null,null,"spliceRow"),g},a.DataMap.prototype.get=function(b,c){if(b=a.hooks.run(this.instance,"modifyRow",b),"string"==typeof c&&c.indexOf(".")>-1){var d=c.split("."),e=this.dataSource[b];if(!e)return null;for(var f=0,g=d.length;g>f;f++)if(e=e[d[f]],"undefined"==typeof e)return null;return e}return"function"==typeof c?c(this.dataSource.slice(b,b+1)[0]):this.dataSource[b]?this.dataSource[b][c]:null};var b=a.helper.cellMethodLookupFactory("copyable",!1);a.DataMap.prototype.getCopyable=function(a,c){return b.call(this.instance,a,this.propToCol(c))?this.get(a,c):""},a.DataMap.prototype.set=function(b,c,d,e){if(b=a.hooks.run(this.instance,"modifyRow",b,e||"datamapGet"),"string"==typeof c&&c.indexOf(".")>-1){for(var f=c.split("."),g=this.dataSource[b],h=0,i=f.length-1;i>h;h++)"undefined"==typeof g[f[h]]&&(g[f[h]]={}),g=g[f[h]];g[f[h]]=d}else"function"==typeof c?c(this.dataSource.slice(b,b+1)[0],d):this.dataSource[b][c]=d},a.DataMap.prototype.physicalRowsToLogical=function(b,c){for(var d,e=this.instance.countRows(),f=(e+b)%e,g=[],h=c;e>f&&h;)d=a.hooks.run(this.instance,"modifyRow",f),g.push(d),h--,f++;return g},a.DataMap.prototype.clear=function(){for(var a=0;a=d;d++){for(h=[],f=Math.min(a.col,b.col);g>=f;f++)h.push(j.call(this,d,this.colToProp(f)));i.push(h)}return i},a.DataMap.prototype.getText=function(a,b){return SheetClip.stringify(this.getRange(a,b,this.DESTINATION_RENDERER))},a.DataMap.prototype.getCopyableText=function(a,b){return SheetClip.stringify(this.getRange(a,b,this.DESTINATION_CLIPBOARD_GENERATOR))}}(b),function(a){a.renderers.cellDecorator=function(b,c,d,e,f,g,h){h.className&&(c.className=c.className?c.className+" "+h.className:h.className),h.readOnly&&a.Dom.addClass(c,h.readOnlyCellClassName),h.valid===!1&&h.invalidCellClassName&&a.Dom.addClass(c,h.invalidCellClassName),h.wordWrap===!1&&h.noWordWrapClassName&&a.Dom.addClass(c,h.noWordWrapClassName),!g&&h.placeholder&&a.Dom.addClass(c,h.placeholderCellClassName)}}(b),function(a){var b=function(b,c,d,e,f,g,h){a.renderers.cellDecorator.apply(this,arguments),!g&&h.placeholder&&(g=h.placeholder);var i=a.helper.stringify(g);if(h.rendererTemplate){a.Dom.empty(c);var j=document.createElement("TEMPLATE");j.setAttribute("bind","{{}}"),j.innerHTML=h.rendererTemplate,HTMLTemplateElement.decorate(j),j.model=b.getSourceDataAtRow(d),c.appendChild(j)}else a.Dom.fastInnerText(c,i)};a.renderers.TextRenderer=b,a.renderers.registerRenderer("text",b)}(b),function(a){var b=document.createElement("DIV");b.className="htAutocompleteWrapper";var c=document.createElement("DIV");c.className="htAutocompleteArrow",c.appendChild(document.createTextNode(String.fromCharCode(9660)));var d=function(d,e,f,g,h,i,j){var k=(b.cloneNode(!0),c.cloneNode(!0));if(a.renderers.TextRenderer(d,e,f,g,h,i,j),e.appendChild(k),a.Dom.addClass(e,"htAutocomplete"),e.firstChild||e.appendChild(document.createTextNode(String.fromCharCode(160))),!d.acArrowListener){var m=a.eventManager(d);d.acArrowListener=function(b){a.Dom.hasClass(b.target,"htAutocompleteArrow")&&d.view.wt.getSetting("onCellDblClick",null,new l(f,g),e)},m.addEventListener(d.rootElement,"mousedown",d.acArrowListener),d.addHookOnce("afterDestroy",function(){m.clear()})}};a.AutocompleteRenderer=d,a.renderers.AutocompleteRenderer=d,a.renderers.registerRenderer("autocomplete",d)}(b),function(a){var b=document.createElement("INPUT");b.className="htCheckboxRendererInput",b.type="checkbox",b.setAttribute("autocomplete","off");var c=function(c,d,e,f,g,h,i){var j=a.eventManager(c);"undefined"==typeof i.checkedTemplate&&(i.checkedTemplate=!0),"undefined"==typeof i.uncheckedTemplate&&(i.uncheckedTemplate=!1),a.Dom.empty(d);var k=b.cloneNode(!1);h===i.checkedTemplate||h===a.helper.stringify(i.checkedTemplate)?(k.checked=!0,d.appendChild(k)):h===i.uncheckedTemplate||h===a.helper.stringify(i.uncheckedTemplate)?d.appendChild(k):null===h?(k.className+=" noValue",d.appendChild(k)):a.Dom.fastInnerText(d,"#bad value#"),i.readOnly?j.addEventListener(k,"click",function(a){a.preventDefault()}):(j.addEventListener(k,"mousedown",function(b){a.helper.stopPropagation(b)}),j.addEventListener(k,"mouseup",function(b){a.helper.stopPropagation(b)}),j.addEventListener(k,"change",function(){this.checked?c.setDataAtRowProp(e,g,i.checkedTemplate):c.setDataAtRowProp(e,g,i.uncheckedTemplate)})),c.CheckboxRenderer&&c.CheckboxRenderer.beforeKeyDownHookBound||(c.CheckboxRenderer={beforeKeyDownHookBound:!0},c.addHook("beforeKeyDown",function(b){if(a.Dom.enableImmediatePropagation(b),b.keyCode==a.helper.keyCode.SPACE||b.keyCode==a.helper.keyCode.ENTER)for(var d,e,f,g=c.getSelectedRange(),h=g.getTopLeftCorner(),i=g.getBottomRightCorner(),k=h.row;k<=i.row;k++)for(var l=h.col;l<=i.col;l++)if(d=c.getCell(k,l),f=c.getCellMeta(k,l),e=d.querySelectorAll("input[type=checkbox]"),e.length>0&&!f.readOnly){b.isImmediatePropagationStopped()||(b.stopImmediatePropagation(),b.preventDefault());for(var m=0,n=e.length;n>m;m++)e[m].checked=!e[m].checked,j.fireEvent(e[m],"change")}}))};a.CheckboxRenderer=c,a.renderers.CheckboxRenderer=c,a.renderers.registerRenderer("checkbox",c)}(b),function(a){var b=function(b,c,d,e,f,g,h){a.helper.isNumeric(g)&&("undefined"!=typeof h.language&&numeral.language(h.language),g=numeral(g).format(h.format||"0"),a.Dom.addClass(c,"htNumeric")),a.renderers.TextRenderer(b,c,d,e,f,g,h)};a.NumericRenderer=b,a.renderers.NumericRenderer=b,a.renderers.registerRenderer("numeric",b)}(b),function(a){var b=function(b,c,d,e,f,g,h){a.renderers.TextRenderer.apply(this,arguments),g=c.innerHTML;var i,j=h.hashLength||g.length,k=h.hashSymbol||"*";for(i="";i.split(k).length-1d[2]&&(c=d[0],d[0]=d[2],d[2]=c),d[1]>d[3]&&(c=d[1],d[1]=d[3],d[3]=c),this.instance.populateFromArray(d[0],d[1],a,d[2],d[3],"edit")}else this.instance.populateFromArray(this.row,this.col,a,null,null,"edit")},b.prototype.beginEditing=function(b){this.state==a.EditorState.VIRGIN&&(this.instance.view.scrollViewport(new l(this.row,this.col)),this.instance.view.render(),this.state=a.EditorState.EDITING,b="string"==typeof b?b:this.originalValue,this.setValue(a.helper.stringify(b)),this.open(),this._opened=!0,this.focus(),this.instance.view.render())},b.prototype.finishEditing=function(b,c,d){var e=this;if(d){var f=this._closeCallback;this._closeCallback=function(a){f&&f(a),d(a)}}if(!this.isWaiting()){if(this.state==a.EditorState.VIRGIN)return void this.instance._registerTimeout(setTimeout(function(){e._fireCallbacks(!0)},0));if(this.state==a.EditorState.EDITING){if(b)return this.cancelChanges(),void this.instance.view.render();var g=[[String.prototype.trim.call(this.getValue())]];this.state=a.EditorState.WAITING,this.saveValue(g,c),this.instance.getCellValidator(this.cellProperties)?this.instance.addHookOnce("postAfterValidate",function(b){e.state=a.EditorState.FINISHED,e.discardEditor(b)}):(this.state=a.EditorState.FINISHED,this.discardEditor(!0))}}},b.prototype.cancelChanges=function(){this.state=a.EditorState.FINISHED,this.discardEditor()},b.prototype.discardEditor=function(b){this.state===a.EditorState.FINISHED&&(b===!1&&this.cellProperties.allowInvalid!==!0?(this.instance.selectCell(this.row,this.col),this.focus(),this.state=a.EditorState.EDITING,this._fireCallbacks(!1)):(this.close(),this._opened=!1,this.state=a.EditorState.VIRGIN,this._fireCallbacks(!0)))},b.prototype.isOpened=function(){return this._opened},b.prototype.isWaiting=function(){return this.state===a.EditorState.WAITING},a.editors.BaseEditor=b}(b),function(a){var b=a.editors.BaseEditor.prototype.extend();b.prototype.init=function(){var b=this;this.createElements(),this.eventManager=new a.eventManager(this),this.bindEvents(),this.autoResize=I(),this.instance.addHook("afterDestroy",function(){b.destroy()})},b.prototype.getValue=function(){return this.TEXTAREA.value},b.prototype.setValue=function(a){this.TEXTAREA.value=a};var c=function(b){var c=this,d=c.getActiveEditor(),e=a.helper.keyCode,f=(b.ctrlKey||b.metaKey)&&!b.altKey;if(a.Dom.enableImmediatePropagation(b),b.target===d.TEXTAREA&&!b.isImmediatePropagationStopped()){if(17===b.keyCode||224===b.keyCode||91===b.keyCode||93===b.keyCode)return void b.stopImmediatePropagation();switch(b.keyCode){case e.ARROW_RIGHT:a.Dom.getCaretPosition(d.TEXTAREA)!==d.TEXTAREA.value.length&&b.stopImmediatePropagation();break;case e.ARROW_LEFT:0!==a.Dom.getCaretPosition(d.TEXTAREA)&&b.stopImmediatePropagation();break;case e.ENTER:var g=d.instance.getSelected(),h=!(g[0]===g[2]&&g[1]===g[3]);(f&&!h||b.altKey)&&(d.isOpened()?(d.setValue(d.getValue()+"\n"),d.focus()):d.beginEditing(d.originalValue+"\n"),b.stopImmediatePropagation()),b.preventDefault();break;case e.A:case e.X:case e.C:case e.V:f&&b.stopImmediatePropagation();break;case e.BACKSPACE:case e.DELETE:case e.HOME:case e.END:b.stopImmediatePropagation()}d.autoResize.resize(String.fromCharCode(b.keyCode))}};b.prototype.open=function(){this.refreshDimensions(),this.instance.addHook("beforeKeyDown",c)},b.prototype.close=function(){this.textareaParentStyle.display="none",this.autoResize.unObserve(),document.activeElement===this.TEXTAREA&&this.instance.listen(),this.instance.removeHook("beforeKeyDown",c)},b.prototype.focus=function(){this.TEXTAREA.focus(),a.Dom.setCaretPosition(this.TEXTAREA,this.TEXTAREA.value.length)},b.prototype.createElements=function(){this.TEXTAREA=document.createElement("TEXTAREA"),a.Dom.addClass(this.TEXTAREA,"handsontableInput"),this.textareaStyle=this.TEXTAREA.style,this.textareaStyle.width=0,this.textareaStyle.height=0,this.TEXTAREA_PARENT=document.createElement("DIV"),a.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.appendChild(this.TEXTAREA_PARENT);var b=this;this.instance._registerTimeout(setTimeout(function(){b.refreshDimensions()},0))},b.prototype.checkEditorSection=function(){return this.rowe&&(e=0),0>f&&(f=0),h>0&&parseInt(this.TD.style.borderTopWidth,10)>0&&(e+=1),i>0&&parseInt(this.TD.style.borderLeftWidth,10)>0&&(f+=1),b&&-1!=b?this.textareaParentStyle[b[0]]=b[1]:a.Dom.resetCssTransform(this.textareaParentStyle),this.textareaParentStyle.top=e+"px",this.textareaParentStyle.left=f+"px";var k=this.TD.offsetTop-this.instance.view.wt.wtScrollbars.vertical.getScrollPosition(),l=this.TD.offsetLeft-this.instance.view.wt.wtScrollbars.horizontal.getScrollPosition(),m=a.Dom.innerWidth(this.TD)-8,n=this.instance.view.maximumVisibleElementWidth(l)-10,o=a.Dom.outerHeight(this.TD)-4,p=this.instance.view.maximumVisibleElementHeight(k)-2;parseInt(this.TD.style.borderTopWidth,10)>0&&(o-=1),parseInt(this.TD.style.borderLeftWidth,10)>0&&h>0&&(m-=1),this.TEXTAREA.style.fontSize=a.Dom.getComputedStyle(this.TD).fontSize,this.TEXTAREA.style.fontFamily=a.Dom.getComputedStyle(this.TD).fontFamily,this.autoResize.init(this.TEXTAREA,{minHeight:Math.min(o,p),maxHeight:p,minWidth:Math.min(m,n),maxWidth:n},!0),this.textareaParentStyle.display="block"}},b.prototype.bindEvents=function(){var b=this;this.eventManager.addEventListener(this.TEXTAREA,"cut",function(b){a.helper.stopPropagation(b)}),this.eventManager.addEventListener(this.TEXTAREA,"paste",function(b){a.helper.stopPropagation(b)}),this.instance.addHook("afterScrollVertically",function(){b.refreshDimensions()}),this.instance.addHook("afterColumnResize",function(){b.refreshDimensions(),b.focus()}),this.instance.addHook("afterRowResize",function(){b.refreshDimensions(),b.focus()}),this.instance.addHook("afterDestroy",function(){b.eventManager.clear()})},b.prototype.destroy=function(){this.eventManager.clear()},a.editors.TextEditor=b,a.editors.registerEditor("text",a.editors.TextEditor)}(b),function(b){var c=b.editors.BaseEditor.prototype.extend(),d={},e=function(){this.controls={},this.controls.leftButton=document.createElement("DIV"),this.controls.leftButton.className="leftButton",this.controls.rightButton=document.createElement("DIV"),this.controls.rightButton.className="rightButton",this.controls.upButton=document.createElement("DIV"),this.controls.upButton.className="upButton",this.controls.downButton=document.createElement("DIV"),this.controls.downButton.className="downButton";for(var a in this.controls)this.controls.hasOwnProperty(a)&&this.positionControls.appendChild(this.controls[a])};c.prototype.valueChanged=function(){return this.initValue!=this.getValue()},c.prototype.init=function(){var a=this;this.eventManager=new b.eventManager(this.instance),this.createElements(),this.bindEvents(),this.instance.addHook("afterDestroy",function(){a.destroy()})},c.prototype.getValue=function(){return this.TEXTAREA.value},c.prototype.setValue=function(a){this.initValue=a,this.TEXTAREA.value=a},c.prototype.createElements=function(){this.editorContainer=document.createElement("DIV"),this.editorContainer.className="htMobileEditorContainer",this.cellPointer=document.createElement("DIV"),this.cellPointer.className="cellPointer",this.moveHandle=document.createElement("DIV"),this.moveHandle.className="moveHandle",this.inputPane=document.createElement("DIV"),this.inputPane.className="inputs",this.positionControls=document.createElement("DIV"),this.positionControls.className="positionControls",this.TEXTAREA=document.createElement("TEXTAREA"),b.Dom.addClass(this.TEXTAREA,"handsontableInput"),this.inputPane.appendChild(this.TEXTAREA),this.editorContainer.appendChild(this.cellPointer),this.editorContainer.appendChild(this.moveHandle),this.editorContainer.appendChild(this.inputPane),this.editorContainer.appendChild(this.positionControls),e.call(this),document.body.appendChild(this.editorContainer)},c.prototype.onBeforeKeyDown=function(a){var c=this,d=c.getActiveEditor();if(b.Dom.enableImmediatePropagation(a),a.target===d.TEXTAREA&&!a.isImmediatePropagationStopped()){var e=b.helper.keyCode;switch(a.keyCode){case e.ENTER:d.close(),a.preventDefault();break;case e.BACKSPACE:a.stopImmediatePropagation()}}},c.prototype.open=function(){this.instance.addHook("beforeKeyDown",this.onBeforeKeyDown),b.Dom.addClass(this.editorContainer,"active"),b.Dom.removeClass(this.cellPointer,"hidden"),this.updateEditorPosition()},c.prototype.focus=function(){this.TEXTAREA.focus(),b.Dom.setCaretPosition(this.TEXTAREA,this.TEXTAREA.value.length)},c.prototype.close=function(){this.TEXTAREA.blur(),this.instance.removeHook("beforeKeyDown",this.onBeforeKeyDown),b.Dom.removeClass(this.editorContainer,"active")},c.prototype.scrollToView=function(){var a=this.instance.getSelectedRange().highlight;this.instance.view.scrollViewport(a)},c.prototype.hideCellPointer=function(){b.Dom.hasClass(this.cellPointer,"hidden")||b.Dom.addClass(this.cellPointer,"hidden")},c.prototype.updateEditorPosition=function(c,e){if(c&&e)c=parseInt(c,10),e=parseInt(e,10),this.editorContainer.style.top=e+"px",this.editorContainer.style.left=c+"px";else{var f=this.instance.getSelected(),g=this.instance.getCell(f[0],f[1]);if(d.cellPointer||(d.cellPointer={height:b.Dom.outerHeight(this.cellPointer),width:b.Dom.outerWidth(this.cellPointer)}),d.editorContainer||(d.editorContainer={width:b.Dom.outerWidth(this.editorContainer)}),void 0!==g){var h=this.instance.view.wt.wtScrollbars.horizontal.scrollHandler==a?0:b.Dom.getScrollLeft(this.instance.view.wt.wtScrollbars.horizontal.scrollHandler),i=this.instance.view.wt.wtScrollbars.vertical.scrollHandler==a?0:b.Dom.getScrollTop(this.instance.view.wt.wtScrollbars.vertical.scrollHandler),j=b.Dom.offset(g),k=b.Dom.outerWidth(g),l={x:h,y:i};this.editorContainer.style.top=parseInt(j.top+b.Dom.outerHeight(g)-l.y+d.cellPointer.height,10)+"px",this.editorContainer.style.left=parseInt(a.innerWidth/2-d.editorContainer.width/2,10)+"px",j.left+k/2>parseInt(this.editorContainer.style.left,10)+d.editorContainer.width?this.editorContainer.style.left=a.innerWidth-d.editorContainer.width+"px":j.left+k/2c?e.deselectCell():e.selectCell(c,0),b.preventDefault(),b.stopImmediatePropagation(),d.instance.listen(),d.TEXTAREA.focus())}};b.prototype.open=function(){this.instance.addHook("beforeKeyDown",c),a.editors.TextEditor.prototype.open.apply(this,arguments),this.htEditor.render(),this.cellProperties.strict?(this.htEditor.selectCell(0,0),this.TEXTAREA.style.visibility="hidden"):(this.htEditor.deselectCell(),this.TEXTAREA.style.visibility="visible"),a.Dom.setCaretPosition(this.TEXTAREA,0,this.TEXTAREA.value.length)},b.prototype.close=function(){this.instance.removeHook("beforeKeyDown",c),this.instance.listen(),a.editors.TextEditor.prototype.close.apply(this,arguments)},b.prototype.focus=function(){this.instance.listen(),a.editors.TextEditor.prototype.focus.apply(this,arguments)},b.prototype.beginEditing=function(){var b=this.instance.getSettings().onBeginEditing;b&&b()===!1||a.editors.TextEditor.prototype.beginEditing.apply(this,arguments)},b.prototype.finishEditing=function(){if(this.htEditor.isListening()&&this.instance.listen(),this.htEditor.getSelected()){var b=this.htEditor.getInstance().getValue();void 0!==b&&this.setValue(b)}return a.editors.TextEditor.prototype.finishEditing.apply(this,arguments)},b.prototype.assignHooks=function(){var a=this;this.instance.addHook("afterDestroy",function(){a.htEditor&&a.htEditor.destroy()})},a.editors.HandsontableEditor=b,a.editors.registerEditor("handsontable",b)}(b),function(b){var c=b.editors.HandsontableEditor.prototype.extend();c.prototype.init=function(){b.editors.HandsontableEditor.prototype.init.apply(this,arguments);var a=this.htEditor.getInstance();a.updateSettings({height:1}),this.query=null,this.choices=[]},c.prototype.createElements=function(){b.editors.HandsontableEditor.prototype.createElements.apply(this,arguments);var c=function(){return-1!=a.navigator.platform.indexOf("Mac")?"htMacScroll":""};b.Dom.addClass(this.htContainer,"autocompleteEditor"),b.Dom.addClass(this.htContainer,c())};var d=!1,e=function(a){d=!1;var c=this.getActiveEditor(),e=b.helper.keyCode;if(b.helper.isPrintableChar(a.keyCode)||a.keyCode===e.BACKSPACE||a.keyCode===e.DELETE||a.keyCode===e.INSERT){var f=0;if(a.keyCode===e.C&&(a.ctrlKey||a.metaKey))return;c.isOpened()||(f+=10),c.instance._registerTimeout(setTimeout(function(){c.queryChoices(c.TEXTAREA.value),d=!0},f))}};c.prototype.prepare=function(){this.instance.addHook("beforeKeyDown",e),b.editors.HandsontableEditor.prototype.prepare.apply(this,arguments)},c.prototype.open=function(){b.editors.HandsontableEditor.prototype.open.apply(this,arguments),this.TEXTAREA.style.visibility="visible",this.focus(),this.htContainer.style.overflow="hidden";var a=this.htEditor.getInstance(),c=this;a.updateSettings({colWidths:[b.Dom.outerWidth(this.TEXTAREA)-2],afterRenderer:function(a,b,d,e,f){var g=this.getCellMeta(b,d).filteringCaseSensitive===!0;if(f){var h=g?f.indexOf(this.query):f.toLowerCase().indexOf(c.query.toLowerCase());if(-1!=h){var i=f.substr(h,c.query.length);a.innerHTML=f.replace(i,""+i+"")}}}}),d&&(d=!1),c.instance._registerTimeout(setTimeout(function(){c.queryChoices(c.TEXTAREA.value),c.htContainer.style.overflow="auto"},0))},c.prototype.close=function(){b.editors.HandsontableEditor.prototype.close.apply(this,arguments)},c.prototype.queryChoices=function(a){if(this.query=a,"function"==typeof this.cellProperties.source){var b=this;this.cellProperties.source(a,function(a){b.updateChoicesList(a)})}else if(Array.isArray(this.cellProperties.source)){var c;if(a&&this.cellProperties.filter!==!1){var d=this.cellProperties.filteringCaseSensitive===!0,e=a.toLowerCase();c=this.cellProperties.source.filter(function(b){return d?-1!=b.indexOf(a):-1!=b.toLowerCase().indexOf(e)})}else c=this.cellProperties.source;this.updateChoicesList(c)}else this.updateChoicesList([])},c.prototype.updateChoicesList=function(a){var d,e=b.Dom.getCaretPosition(this.TEXTAREA),f=b.Dom.getSelectionEndPosition(this.TEXTAREA),g=c.sortByRelevance(this.getValue(),a,this.cellProperties.filteringCaseSensitive);if(0!=this.cellProperties.filter){for(var h=[],i=0,j=g.length;j>i;i++)h.push(a[g[i]]);d=0,a=h}else d=g[0];this.choices=a,this.htEditor.loadData(b.helper.pivot([a])),this.htEditor.updateSettings({height:this.getDropdownHeight()}),this.cellProperties.strict===!0&&this.highlightBestMatchingChoice(d),this.instance.listen(),this.TEXTAREA.focus(),b.Dom.setCaretPosition(this.TEXTAREA,e,e!=f?f:void 0)},c.prototype.finishEditing=function(a){a||this.instance.removeHook("beforeKeyDown",e),b.editors.HandsontableEditor.prototype.finishEditing.apply(this,arguments)},c.prototype.highlightBestMatchingChoice=function(a){"number"==typeof a?this.htEditor.selectCell(a,0):this.htEditor.deselectCell()},c.sortByRelevance=function(a,b,c){var d,e,f,g,h,i=[],j=a.length,k=[];if(0===j){for(g=0,h=b.length;h>g;g++)k.push(g);return k}for(g=0,h=b.length;h>g;g++)d=b[g],e=c?d.indexOf(a):d.toLowerCase().indexOf(a.toLowerCase()),-1!=e&&(f=d.length-e-j,i.push({baseIndex:g,index:e,charsLeft:f,value:d}));for(i.sort(function(a,b){return-1===b.index?-1:-1===a.index?1:a.indexb.charsLeft?1:0:void 0}),g=0,h=i.length;h>g;g++)k.push(i[g].baseIndex);return k},c.prototype.getDropdownHeight=function(){var a=this.htEditor.getInstance().getRowHeight(0)||23;return this.choices.length>=10?10*a:this.choices.length*a+8},b.editors.AutocompleteEditor=c,b.editors.registerEditor("autocomplete",c)}(b),function(a){var b=a.editors.TextEditor.prototype.extend();b.prototype.createElements=function(){a.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,a.Dom.empty(this.TEXTAREA_PARENT),this.TEXTAREA_PARENT.appendChild(this.TEXTAREA)},a.editors.PasswordEditor=b,a.editors.registerEditor("password",b)}(b),function(a){var b=a.editors.BaseEditor.prototype.extend();b.prototype.init=function(){this.select=document.createElement("SELECT"),a.Dom.addClass(this.select,"htSelectEditor"),this.select.style.display="none",this.instance.rootElement.appendChild(this.select)},b.prototype.prepare=function(){a.editors.BaseEditor.prototype.prepare.apply(this,arguments);var b,c=this.cellProperties.selectOptions;b=this.prepareOptions("function"==typeof c?c(this.row,this.col,this.prop):c),a.Dom.empty(this.select);for(var d in b)if(b.hasOwnProperty(d)){var e=document.createElement("OPTION");e.value=d,a.Dom.fastInnerHTML(e,b[d]),this.select.appendChild(e)}},b.prototype.prepareOptions=function(a){var b={};if(Array.isArray(a))for(var c=0,d=a.length;d>c;c++)b[a[c]]=a[c];else"object"==typeof a&&(b=a);return b},b.prototype.getValue=function(){return this.select.value},b.prototype.setValue=function(a){this.select.value=a};var c=function(b){var c=this,d=c.getActiveEditor();switch(b.keyCode){case a.helper.keyCode.ARROW_UP:var e=d.select.find("option:selected").prev();1==e.length&&e.prop("selected",!0),b.stopImmediatePropagation(),b.preventDefault();break;case a.helper.keyCode.ARROW_DOWN:var f=d.select.find("option:selected").next();1==f.length&&f.prop("selected",!0),b.stopImmediatePropagation(),b.preventDefault()}};b.prototype.checkEditorSection=function(){return this.rowf;f++){if(c===a[f]){e=!0;break}if(d===a[f].toLowerCase()){e=!0;break}}b(e)}};b.AutocompleteValidator=function(a,b){this.strict&&this.source?"function"==typeof this.source?this.source(a,H(a,b)):H(a,b)(this.source):b(!0)},b.mobileBrowser=b.helper.isMobileBrowser(),b.AutocompleteCell={editor:b.editors.AutocompleteEditor,renderer:b.renderers.AutocompleteRenderer,validator:b.AutocompleteValidator},b.CheckboxCell={editor:b.editors.CheckboxEditor,renderer:b.renderers.CheckboxRenderer},b.TextCell={editor:b.mobileBrowser?b.editors.MobileTextEditor:b.editors.TextEditor,renderer:b.renderers.TextRenderer},b.NumericCell={editor:b.editors.NumericEditor,renderer:b.renderers.NumericRenderer,validator:b.NumericValidator,dataType:"number"},b.DateCell={editor:b.editors.DateEditor,renderer:b.renderers.AutocompleteRenderer},b.HandsontableCell={editor:b.editors.HandsontableEditor,renderer:b.renderers.AutocompleteRenderer},b.PasswordCell={editor:b.editors.PasswordEditor,renderer:b.renderers.PasswordRenderer,copyable:!1},b.DropdownCell={editor:b.editors.DropdownEditor,renderer:b.renderers.AutocompleteRenderer,validator:b.AutocompleteValidator},b.cellTypes={text:b.TextCell,date:b.DateCell,numeric:b.NumericCell,checkbox:b.CheckboxCell,autocomplete:b.AutocompleteCell,handsontable:b.HandsontableCell,password:b.PasswordCell,dropdown:b.DropdownCell},b.cellLookup={validator:{numeric:b.NumericValidator,autocomplete:b.AutocompleteValidator}};var I=function(){var c,d={minHeight:200,maxHeight:300,minWidth:100,maxWidth:300},e=document.body,f=document.createTextNode(""),g=document.createElement("SPAN"),h=function(b,c,d){a.attachEvent?b.attachEvent("on"+c,d):b.addEventListener(c,d,!1)},i=function(b,c,d){a.removeEventListener?b.removeEventListener(c,d,!1):b.detachEvent("on"+c,d)},j=function(a){var h,i;a?/^[a-zA-Z \.,\\\/\|0-9]$/.test(a)||(a="."):a="",void 0!==f.textContent?f.textContent=c.value+a:f.data=c.value+a,g.style.fontSize=b.Dom.getComputedStyle(c).fontSize,g.style.fontFamily=b.Dom.getComputedStyle(c).fontFamily,g.style.whiteSpace="pre",e.appendChild(g),h=g.clientWidth+2,e.removeChild(g),c.style.height=d.minHeight+"px",c.style.width=d.minWidth>h?d.minWidth+"px":h>d.maxWidth?d.maxWidth+"px":h+"px",i=c.scrollHeight?c.scrollHeight-1:0,d.minHeight>i?c.style.height=d.minHeight+"px":d.maxHeight1&&""===e[e.length-1]&&e.pop(),c=0,d=e.length;d>c;c+=1){for(e[c]=e[c].split(" "),f=0,g=e[c].length;g>f;f+=1)j[k]||(j[k]=[]),h&&0===f?(i=j[k].length-1,j[k][i]=j[k][i]+"\n"+e[c][0],h&&1&b(e[c][0])&&(h=!1,j[k][i]=j[k][i].substring(0,j[k][i].length-1).replace(/""/g,'"'))):f===g-1&&0===e[c][f].indexOf('"')?(j[k].push(e[c][f].substring(1).replace(/""/g,'"')),h=!0):(j[k].push(e[c][f].replace(/""/g,'"')),h=!1);h||(k+=1)}return j},stringify:function(a){var b,c,d,e,f,g="";for(b=0,c=a.length;c>b;b+=1){for(e=a[b].length,d=0;e>d;d+=1)d>0&&(g+=" "),f=a[b][d],g+="string"==typeof f?f.indexOf("\n")>-1?'"'+f.replace(/"/g,'""')+'"':f:null===f||void 0===f?"":f;g+="\n"}return g}}}(a);var J=function(){var a;return{getInstance:function(){return a?a.hasBeenDestroyed()&&a.init():a=new c,a.refCounter++,a}}}();c.prototype.init=function(){var a,c;this.copyCallbacks=[],this.cutCallbacks=[],this.pasteCallbacks=[],this._eventManager=b.eventManager(this),c=document.body,document.getElementById("CopyPasteDiv")?(this.elDiv=document.getElementById("CopyPasteDiv"),this.elTextarea=this.elDiv.firstChild):(this.elDiv=document.createElement("DIV"),this.elDiv.id="CopyPasteDiv",a=this.elDiv.style,a.position="fixed",a.top="-10000px",a.left="-10000px",c.appendChild(this.elDiv),this.elTextarea=document.createElement("TEXTAREA"),this.elTextarea.className="copyPaste",this.elTextarea.onpaste=function(a){return"WebkitAppearance"in document.documentElement.style?(this.value=a.clipboardData.getData("Text"),!1):void 0},a=this.elTextarea.style,a.width="10000px",a.height="10000px",a.overflow="hidden",this.elDiv.appendChild(this.elTextarea),"undefined"!=typeof a.opacity&&(a.opacity=0)),this.keyDownRemoveEvent=this._eventManager.addEventListener(document.documentElement,"keydown",this.onKeyDown.bind(this),!1)},c.prototype.onKeyDown=function(a){var b=this,c=!1;if(a.metaKey?c=!0:a.ctrlKey&&-1===navigator.userAgent.indexOf("Mac")&&(c=!0),c){if(document.activeElement!==this.elTextarea&&(""!==this.getSelectionText()||-1!==["INPUT","SELECT","TEXTAREA"].indexOf(document.activeElement.nodeName)))return;this.selectNodeText(this.elTextarea),setTimeout(function(){b.selectNodeText(b.elTextarea)},0)}!c||67!==a.keyCode&&86!==a.keyCode&&88!==a.keyCode||(88===a.keyCode?setTimeout(function(){b.triggerCut(a)},0):86===a.keyCode&&setTimeout(function(){b.triggerPaste(a)},0))},c.prototype.selectNodeText=function(a){a&&a.select()},c.prototype.getSelectionText=function(){var b="";return a.getSelection?b=a.getSelection().toString():document.selection&&"Control"!=document.selection.type&&(b=document.selection.createRange().text),b},c.prototype.copyable=function(a){if("string"!=typeof a&&void 0===a.toString)throw new Error("copyable requires string parameter");this.elTextarea.value=a},c.prototype.onCut=function(a){this.cutCallbacks.push(a)},c.prototype.onPaste=function(a){this.pasteCallbacks.push(a)},c.prototype.removeCallback=function(a){var b,c;for(b=0,c=this.copyCallbacks.length;c>b;b++)if(this.copyCallbacks[b]===a)return this.copyCallbacks.splice(b,1),!0;for(b=0,c=this.cutCallbacks.length;c>b;b++)if(this.cutCallbacks[b]===a)return this.cutCallbacks.splice(b,1),!0;for(b=0,c=this.pasteCallbacks.length;c>b;b++)if(this.pasteCallbacks[b]===a)return this.pasteCallbacks.splice(b,1),!0;return!1},c.prototype.triggerCut=function(a){var b=this;b.cutCallbacks&&setTimeout(function(){for(var c=0,d=b.cutCallbacks.length;d>c;c++)b.cutCallbacks[c](a)},50)},c.prototype.triggerPaste=function(a,b){var c=this;c.pasteCallbacks&&setTimeout(function(){for(var d=b||c.elTextarea.value,e=0,f=c.pasteCallbacks.length;f>e;e++)c.pasteCallbacks[e](d,a)},50)},c.prototype.destroy=function(){this.hasBeenDestroyed()||0!==--this.refCounter||(this.elDiv&&this.elDiv.parentNode&&(this.elDiv.parentNode.removeChild(this.elDiv),this.elDiv=null,this.elTextarea=null),this.keyDownRemoveEvent())},c.prototype.hasBeenDestroyed=function(){return!this.refCounter};var K;!function(b){function c(a){return-1===a.indexOf("/")&&-1===a.indexOf("~")?a:a.replace(/~/g,"~0").replace(/\//g,"~1")}function d(a,b){var e;for(var f in a)if(a.hasOwnProperty(f)){if(a[f]===b)return c(f)+"/";if("object"==typeof a[f]&&(e=d(a[f],b),""!=e))return c(f)+"/"+e}return""}function e(a,b){if(a===b)return"/";var c=d(a,b);if(""===c)throw new Error("Object not found in root");return"/"+c}function f(a){for(var b=0,c=s.length;c>b;b++)if(s[b].obj===a)return s[b]}function g(a,b){for(var c=0,d=a.observers.length;d>c;c++)if(a.observers[c].callback===b)return a.observers[c].observer}function h(a,b){for(var c=0,d=a.observers.length;d>c;c++)if(a.observers[c].observer===b)return void a.observers.splice(c,1)}function i(a,b){m(b),Object.observe?l(b,a):clearTimeout(b.next);var c=f(a);h(c,b)}function j(b,c){var d,h=[],i=b,j=f(b);if(j?d=g(j,c):(j=new t(b),s.push(j)),d)return d;if(Object.observe)d=function(a){l(d,b),k(d,b);for(var f=0,g=a.length;g>f;){if(("length"!==a[f].name||!w(a[f].object))&&"__Jasmine_been_here_before__"!==a[f].name){var j=a[f].type;switch(j){case"new":j="add";break;case"deleted":j="delete";break;case"updated":j="update"}r[j].call(a[f],h,e(i,a[f].object))}f++}h&&c&&c(h),d.patches=h,h=[]};else if(d={},j.value=JSON.parse(JSON.stringify(b)),c){d.callback=c,d.next=null;var n=this.intervals||[100,1e3,1e4,6e4],o=0,p=function(){m(d) +},q=function(){clearTimeout(d.next),d.next=setTimeout(function(){p(),o=0,d.next=setTimeout(v,n[o++])},0)},v=function(){p(),o==n.length&&(o=n.length-1),d.next=setTimeout(v,n[o++])};"undefined"!=typeof a&&(a.addEventListener?(a.addEventListener("mousedown",q),a.addEventListener("mouseup",q),a.addEventListener("keydown",q)):(a.attachEvent("onmousedown",q),a.attachEvent("onmouseup",q),a.attachEvent("onkeydown",q))),d.next=setTimeout(v,n[o++])}return d.patches=h,d.object=b,j.observers.push(new u(c,d)),k(d,b)}function k(a,b){if(Object.observe){Object.observe(b,a);for(var c in b)if(b.hasOwnProperty(c)){var d=b[c];d&&"object"==typeof d&&k(a,d)}}return a}function l(a,b){if(Object.observe){Object.unobserve(b,a);for(var c in b)if(b.hasOwnProperty(c)){var d=b[c];d&&"object"==typeof d&&l(a,d)}}return a}function m(a){if(Object.observe)Object.deliverChangeRecords(a);else{for(var b,c=0,d=s.length;d>c;c++)if(s[c].obj===a.object){b=s[c];break}n(b.value,a.object,a.patches,"")}var e=a.patches;return e.length>0&&(a.patches=[],a.callback&&a.callback(e)),e}function n(a,b,d,e){for(var f=v(b),g=v(a),h=!1,i=!1,j=g.length-1;j>=0;j--){var k=g[j],l=a[k];if(b.hasOwnProperty(k)){var m=b[k];l instanceof Object?n(l,m,d,e+"/"+c(k)):l!=m&&(h=!0,d.push({op:"replace",path:e+"/"+c(k),value:m}),a[k]=m)}else d.push({op:"remove",path:e+"/"+c(k)}),delete a[k],i=!0}if(i||f.length!=g.length)for(var j=0;je;){c=b[e];for(var g=c.path.split("/"),h=a,i=1,j=g.length;;)if(w(h)){var k=parseInt(g[i],10);if(i++,i>=j){d=q[c.op].call(c,h,k,a);break}h=h[k]}else{var l=g[i];if(-1!=l.indexOf("~")&&(l=l.replace(/~1/g,"/").replace(/~0/g,"~")),i++,i>=j){d=p[c.op].call(c,h,l,a);break}h=h[l]}e++}return d}var p={add:function(a,b){return a[b]=this.value,!0},remove:function(a,b){return delete a[b],!0},replace:function(a,b){return a[b]=this.value,!0},move:function(a,b,c){var d={op:"_get",path:this.from};return o(c,[d]),o(c,[{op:"remove",path:this.from}]),o(c,[{op:"add",path:this.path,value:d.value}]),!0},copy:function(a,b,c){var d={op:"_get",path:this.from};return o(c,[d]),o(c,[{op:"add",path:this.path,value:d.value}]),!0},test:function(a,b){return JSON.stringify(a[b])===JSON.stringify(this.value)},_get:function(a,b){this.value=a[b]}},q={add:function(a,b){return a.splice(b,0,this.value),!0},remove:function(a,b){return a.splice(b,1),!0},replace:function(a,b){return a[b]=this.value,!0},move:p.move,copy:p.copy,test:p.test,_get:p._get},r={add:function(a,b){var d={op:"add",path:b+c(this.name),value:this.object[this.name]};a.push(d)},"delete":function(a,b){var d={op:"remove",path:b+c(this.name)};a.push(d)},update:function(a,b){var d={op:"replace",path:b+c(this.name),value:this.object[this.name]};a.push(d)}},s=[];b.intervals;var t=function(){function a(a){this.observers=[],this.obj=a}return a}(),u=function(){function a(a,b){this.callback=a,this.observer=b}return a}();b.unobserve=i,b.observe=j,b.generate=m;var v;v=Object.keys?Object.keys:function(a){var b=[];for(var c in a)a.hasOwnProperty(c)&&b.push(c);return b};var w;w=Array.isArray?Array.isArray:function(a){return a.push&&"number"==typeof a.length},b.apply=o}(K||(K={})),"undefined"!=typeof exports&&(exports.apply=K.apply,exports.observe=K.observe,exports.unobserve=K.unobserve,exports.generate=K.generate),b.PluginHookClass=function(){function a(){this.hooks=b(),this.globalBucket={},this.legacy=c}var b=function(){return{beforeInitWalkontable:[],beforeInit:[],beforeRender:[],beforeSetRangeEnd:[],beforeDrawBorders:[],beforeChange:[],beforeChangeRender:[],beforeRemoveCol:[],beforeRemoveRow:[],beforeValidate:[],beforeGetCellMeta:[],beforeAutofill:[],beforeKeyDown:[],beforeOnCellMouseDown:[],beforeTouchScroll:[],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:[],afterIsMultipleSelectionCheck:[],afterDocumentKeyDown:[],afterMomentumScroll:[],modifyColWidth:[],modifyRowHeight:[],modifyRow:[],modifyCol:[]}},c={onBeforeChange:"beforeChange",onChange:"afterChange",onCreateRow:"afterCreateRow",onCreateCol:"afterCreateCol",onSelection:"afterSelection",onCopyLimit:"afterCopyLimit",onSelectionEnd:"afterSelectionEnd",onSelectionByProp:"afterSelectionByProp",onSelectionEndByProp:"afterSelectionEndByProp"};return a.prototype.getBucket=function(a){return a?(a.pluginHookBucket||(a.pluginHookBucket={}),a.pluginHookBucket):this.globalBucket},a.prototype.add=function(a,b,d){if(Array.isArray(b))for(var e=0,f=b.length;f>e;e++)this.add(a,b[e]);else{a in c&&(a=c[a]);var g=this.getBucket(d);"undefined"==typeof g[a]&&(g[a]=[]),b.skip=!1,-1==g[a].indexOf(b)&&g[a].push(b)}return this},a.prototype.once=function(a,b,c){if(Array.isArray(b))for(var d=0,e=b.length;e>d;d++)b[d].runOnce=!0,this.add(a,b[d],c);else b.runOnce=!0,this.add(a,b,c)},a.prototype.remove=function(a,b,d){var e=!1;a in c&&(a=c[a]);var f=this.getBucket(d);if("undefined"!=typeof f[a])for(var g=0,h=f[a].length;h>g;g++)if(f[a][g]==b){f[a][g].skip=!0,e=!0;break}return e},a.prototype.destroy=function(a){var b=this.getBucket(a);for(var c in b)if(b.hasOwnProperty(c))for(var d=0,e=b[c].length;e>d;d++)this.remove(c,b[c],a)},a.prototype.run=function(a,b,d,e,f,g,h,i){return b in c&&(b=c[b]),d=this._runBucket(this.globalBucket,a,b,d,e,f,g,h,i),d=this._runBucket(this.getBucket(a),a,b,d,e,f,g,h,i)},a.prototype._runBucket=function(a,b,c,d,e,f,g,h,i){var j,k,l,m=a[c];if(m)for(k=0,l=m.length;l>k;k++)m[k].skip||(j=m[k].call(b,d,e,f,g,h,i),void 0!==j&&(d=j),m[k].runOnce&&this.remove(c,m[k],a===this.globalBucket?null:b));return d},a.prototype.register=function(a){this.isRegistered(a)||(this.hooks[a]=[])},a.prototype.deregister=function(a){delete this.hooks[a]},a.prototype.isRegistered=function(a){return"undefined"!=typeof this.hooks[a]},a.prototype.getRegistered=function(){return Object.keys(this.hooks)},a}(),b.hooks=new b.PluginHookClass,b.PluginHooks=b.hooks,function(a){function b(){function b(a){var b=document,c=this;c.table=b.createElement("table"),c.theadTh=b.createElement("th"),c.table.appendChild(b.createElement("thead")).appendChild(b.createElement("tr")).appendChild(c.theadTh),c.tableStyle=c.table.style,c.tableStyle.tableLayout="auto",c.tableStyle.width="auto",c.tbody=b.createElement("tbody"),c.table.appendChild(c.tbody),c.container=b.createElement("div"),c.container.className=a.rootElement.className+" hidden",c.containerStyle=c.container.style,c.container.appendChild(c.table)}var d=this,e=5;this.beforeInit=function(){var a=this;a.autoColumnWidths=[],a.getSettings().autoColumnSize!==!1?a.autoColumnSizeTmp||(a.autoColumnSizeTmp={table:null,tableStyle:null,theadTh:null,tbody:null,container:null,containerStyle:null,determineBeforeNextRender:!0},a.addHook("beforeRender",c.determineIfChanged),a.addHook("modifyColWidth",c.modifyColWidth),a.addHook("afterDestroy",c.afterDestroy),a.determineColumnWidth=d.determineColumnWidth):a.autoColumnSizeTmp&&(a.removeHook("beforeRender",c.determineIfChanged),a.removeHook("modifyColWidth",c.modifyColWidth),a.removeHook("afterDestroy",c.afterDestroy),delete a.determineColumnWidth,d.afterDestroy.call(a))},this.determineIfChanged=function(a){a&&c.determineColumnsWidth.apply(this,arguments)},this.determineColumnWidth=function(c){var d=this,f=d.autoColumnSizeTmp;f.container||b.call(f,d),f.container.className=d.rootElement.className+" htAutoColumnSize",f.table.className=d.table.className;for(var g=d.countRows(),h={},i=0,j=0;g>j;j++){var k=a.helper.stringify(d.getDataAtCell(j,c)),l=k.length;l>i&&(i=l),h[l]||(h[l]={needed:e,strings:[]}),h[l].needed&&(h[l].strings.push({value:k,row:j}),h[l].needed--)}var m=d.getSettings();m.colHeaders&&d.view.appendColHeader(c,f.theadTh),a.Dom.empty(f.tbody);for(var n in h)if(h.hasOwnProperty(n))for(var o=0,p=h[n].strings.length;p>o;o++){var q=h[n].strings[o].row,r=d.getCellMeta(q,c);r.col=c,r.row=q;var s=d.getCellRenderer(r),t=document.createElement("tr"),u=document.createElement("td");s(d,u,q,c,d.colToProp(c),h[n].strings[o].value,r),j++,t.appendChild(u),f.tbody.appendChild(t)}var v=d.rootElement.parentNode;v.appendChild(f.container);var w=a.Dom.outerWidth(f.table);return v.removeChild(f.container),w},this.determineColumnsWidth=function(){var a=this,b=this.getSettings();if(b.autoColumnSize||!b.colWidths)for(var c=this.countCols(),e=0;c>e;e++)a._getColWidthFromSettings(e)||(this.autoColumnWidths[e]=d.determineColumnWidth.call(a,e))},this.modifyColWidth=function(a,b){return this.autoColumnWidths[b]&&this.autoColumnWidths[b]>a?this.autoColumnWidths[b]:a},this.afterDestroy=function(){var a=this;a.autoColumnSizeTmp&&a.autoColumnSizeTmp.container&&a.autoColumnSizeTmp.container.parentNode&&a.autoColumnSizeTmp.container.parentNode.removeChild(a.autoColumnSizeTmp.container),a.autoColumnSizeTmp=null}}var c=new b;a.hooks.add("beforeInit",c.beforeInit),a.hooks.add("afterUpdateSettings",c.beforeInit)}(b);var L=new d;b.hooks.add("afterInit",function(){L.init.call(this,"afterInit")}),b.hooks.add("afterUpdateSettings",function(){L.init.call(this,"afterUpdateSettings")}),b.hooks.add("modifyRow",L.translateRow),b.hooks.add("afterGetColHeader",L.getColHeader),b.hooks.register("beforeColumnSort"),b.hooks.register("afterColumnSort"),function(a){function b(a,b){return-1!=a.indexOf(b)?a:(a=a.replace("htTop","").replace("htMiddle","").replace("htBottom","").replace(" ",""),a+=" "+b)}function c(a,b){return-1!=a.indexOf(b)?a:(a=a.replace("htLeft","").replace("htCenter","").replace("htRight","").replace("htJustify","").replace(" ",""),a+=" "+b)}function d(a,d,e,f){var g=this.getCellMeta(a,d),h=f;g.className&&(h="vertical"===e?b(g.className,f):c(g.className,f)),this.setCellMeta(a,d,"className",h)}function e(a,b,c){if(a.from.row==a.to.row&&a.from.col==a.to.col)d.call(this,a.from.row,a.from.col,b,c);else for(var e=a.from.row;e<=a.to.row;e++)for(var f=a.from.col;f<=a.to.col;f++)d.call(this,e,f,b,c);this.render()}function f(b){this.instance=b;var c=this;if(c.menus=[],c.htMenus={},c.triggerRows=[],c.eventManager=a.eventManager(c),this.enabled=!0,this.instance.addHook("afterDestroy",function(){c.destroy()}),this.defaultOptions={items:[{key:"row_above",name:"Insert row above",callback:function(a,b){this.alter("insert_row",b.start.row)},disabled:function(){var a=this.getSelected(),b=[0,a[1],this.countRows()-1,a[1]],c=b.join(",")==a.join(",");return a[0]<0||this.countRows()>=this.getSettings().maxRows||c}},{key:"row_below",name:"Insert row below",callback:function(a,b){this.alter("insert_row",b.end.row+1)},disabled:function(){var a=this.getSelected(),b=[0,a[1],this.countRows()-1,a[1]],c=b.join(",")==a.join(",");return this.getSelected()[0]<0||this.countRows()>=this.getSettings().maxRows||c}},f.SEPARATOR,{key:"col_left",name:"Insert column on the left",callback:function(a,b){this.alter("insert_col",b.start.col)},disabled:function(){var a=this.getSelected(),b=[a[0],0,a[0],this.countCols()-1],c=b.join(",")==a.join(",");return this.getSelected()[1]<0||this.countCols()>=this.getSettings().maxCols||c}},{key:"col_right",name:"Insert column on the right",callback:function(a,b){this.alter("insert_col",b.end.col+1)},disabled:function(){var a=this.getSelected(),b=[a[0],0,a[0],this.countCols()-1],c=b.join(",")==a.join(",");return a[1]<0||this.countCols()>=this.getSettings().maxCols||c}},f.SEPARATOR,{key:"remove_row",name:"Remove row",callback:function(a,b){var c=b.end.row-b.start.row+1;this.alter("remove_row",b.start.row,c)},disabled:function(){var a=this.getSelected(),b=[0,a[1],this.countRows()-1,a[1]],c=b.join(",")==a.join(",");return a[0]<0||c}},{key:"remove_col",name:"Remove column",callback:function(a,b){var c=b.end.col-b.start.col+1;this.alter("remove_col",b.start.col,c)},disabled:function(){var a=this.getSelected(),b=[a[0],0,a[0],this.countCols()-1],c=b.join(",")==a.join(",");return a[1]<0||c}},f.SEPARATOR,{key:"undo",name:"Undo",callback:function(){this.undo()},disabled:function(){return this.undoRedo&&!this.undoRedo.isUndoAvailable()}},{key:"redo",name:"Redo",callback:function(){this.redo()},disabled:function(){return this.undoRedo&&!this.undoRedo.isRedoAvailable()}},f.SEPARATOR,{key:"make_read_only",name:function(){var a="Read only",b=c.checkSelectionReadOnlyConsistency(this);return b&&(a=c.markSelected(a)),a},callback:function(){var a=c.checkSelectionReadOnlyConsistency(this),b=this;this.getSelectedRange().forAll(function(c,d){b.getCellMeta(c,d).readOnly=a?!1:!0}),this.render()}},f.SEPARATOR,{key:"alignment",name:"Alignment",submenu:{items:[{name:function(){var a="Left",b=c.checkSelectionAlignment(this,"htLeft");return b&&(a=c.markSelected(a)),a},callback:function(){e.call(this,this.getSelectedRange(),"horizontal","htLeft")},disabled:!1},{name:function(){var a="Center",b=c.checkSelectionAlignment(this,"htCenter");return b&&(a=c.markSelected(a)),a},callback:function(){e.call(this,this.getSelectedRange(),"horizontal","htCenter")},disabled:!1},{name:function(){var a="Right",b=c.checkSelectionAlignment(this,"htRight");return b&&(a=c.markSelected(a)),a},callback:function(){e.call(this,this.getSelectedRange(),"horizontal","htRight")},disabled:!1},{name:function(){var a="Justify",b=c.checkSelectionAlignment(this,"htJustify");return b&&(a=c.markSelected(a)),a},callback:function(){e.call(this,this.getSelectedRange(),"horizontal","htJustify")},disabled:!1},f.SEPARATOR,{name:function(){var a="Top",b=c.checkSelectionAlignment(this,"htTop");return b&&(a=c.markSelected(a)),a},callback:function(){e.call(this,this.getSelectedRange(),"vertical","htTop")},disabled:!1},{name:function(){var a="Middle",b=c.checkSelectionAlignment(this,"htMiddle");return b&&(a=c.markSelected(a)),a},callback:function(){e.call(this,this.getSelectedRange(),"vertical","htMiddle")},disabled:!1},{name:function(){var a="Bottom",b=c.checkSelectionAlignment(this,"htBottom");return b&&(a=c.markSelected(a)),a},callback:function(){e.call(this,this.getSelectedRange(),"vertical","htBottom")},disabled:!1}]}}]},c.options={},a.helper.extend(c.options,this.options),this.bindMouseEvents(),this.markSelected=function(a){return""+String.fromCharCode(10003)+""+a},this.checkSelectionAlignment=function(a,b){var c=!1;return a.getSelectedRange().forAll(function(d,e){var f=a.getCellMeta(d,e).className;return f&&-1!=f.indexOf(b)?(c=!0,!1):void 0}),c},!this.instance.getSettings().allowInsertRow){var d=h(this.defaultOptions.items,"row_above");this.defaultOptions.items.splice(d,1);var g=h(this.defaultOptions.items,"row_above");this.defaultOptions.items.splice(g,1),this.defaultOptions.items.splice(g,1)}if(!this.instance.getSettings().allowInsertColumn){var i=h(this.defaultOptions.items,"col_left");this.defaultOptions.items.splice(i,1);var j=h(this.defaultOptions.items,"col_right");this.defaultOptions.items.splice(j,1),this.defaultOptions.items.splice(j,1)}var k,l,m=!1,n=!1;this.instance.getSettings().allowRemoveRow||(k=h(this.defaultOptions.items,"remove_row"),this.defaultOptions.items.splice(k,1),m=!0),this.instance.getSettings().allowRemoveColumn||(l=h(this.defaultOptions.items,"remove_col"),this.defaultOptions.items.splice(l,1),n=!0),m&&n&&this.defaultOptions.items.splice(l,1),this.checkSelectionReadOnlyConsistency=function(a){var b=!1;return a.getSelectedRange().forAll(function(c,d){return a.getCellMeta(c,d).readOnly?(b=!0,!1):void 0}),b},a.hooks.run(b,"afterContextMenuDefaultOptions",this.defaultOptions)}function g(a,b){for(var c=0,d=a.length;d>c;c++)if(a[c].key===b)return a[c]}function h(a,b){for(var c=0,d=a.length;d>c;c++)if(a[c].key===b)return c}function i(){if(!this.rootElement.className.indexOf("htContextMenu")){for(var a=0,b=0,c=this.getSettings().data.length,d=0;c>d;d++)this.getSettings().data[d].name==f.SEPARATOR.name?a+=2:b+=26;this.view.wt.wtScrollbars.vertical.fixedContainer.style.height=b+a+"px"}}function j(){var b=this,c=b.getSettings().contextMenu,d=a.helper.isObject(c)?c:{};c?(b.contextMenu||(b.contextMenu=new f(b,d)),b.contextMenu.enable()):b.contextMenu&&(b.contextMenu.destroy(),delete b.contextMenu)}f.prototype.createMenu=function(b,c){b&&(b=b.replace(/ /g,"_"),b="htContextSubMenu_"+b);var d;return d=document.querySelector(b?".htContextMenu."+b:".htContextMenu"),d||(d=document.createElement("DIV"),a.Dom.addClass(d,"htContextMenu"),b&&a.Dom.addClass(d,b),document.getElementsByTagName("body")[0].appendChild(d)),this.menus.indexOf(d)<0&&(this.menus.push(d),c=c||0,this.triggerRows.push(c)),d},f.prototype.bindMouseEvents=function(){function b(b){var c=this.instance.getSettings();this.closeAll(),b.preventDefault(),a.helper.stopPropagation(b);var d=this.instance.getSettings().rowHeaders,e=this.instance.getSettings().colHeaders;if(d||e||"TD"==b.target.nodeName||a.Dom.hasClass(b.target,"current")&&a.Dom.hasClass(b.target,"wtBorder")){var g=this.createMenu(),h=this.getItems(c.contextMenu);this.show(g,h),this.setMenuPosition(b,g),this.eventManager.addEventListener(document.documentElement,"mousedown",a.helper.proxy(f.prototype.closeAll,this))}}var c=a.eventManager(this.instance);c.addEventListener(this.instance.rootElement,"contextmenu",a.helper.proxy(b,this))},f.prototype.bindTableEvents=function(){this._afterScrollCallback=function(){},this.instance.addHook("afterScrollVertically",this._afterScrollCallback),this.instance.addHook("afterScrollHorizontally",this._afterScrollCallback)},f.prototype.unbindTableEvents=function(){this._afterScrollCallback&&(this.instance.removeHook("afterScrollVertically",this._afterScrollCallback),this.instance.removeHook("afterScrollHorizontally",this._afterScrollCallback),this._afterScrollCallback=null)},f.prototype.performAction=function(a,b){var c=this,d=b.getSelected()[0],e=b.getData()[d];if(e.disabled!==!0&&("function"!=typeof e.disabled||e.disabled.call(this.instance)!==!0)&&!e.hasOwnProperty("submenu")){if("function"!=typeof e.callback)return;var g=this.instance.getSelectedRange(),h=f.utils.normalizeSelection(g);e.callback.call(this.instance,e.key,h,a),c.closeAll()}},f.prototype.unbindMouseEvents=function(){this.eventManager.clear();var b=a.eventManager(this.instance);b.removeEventListener(this.instance.rootElement,"contextmenu")},f.prototype.show=function(b,c){var d=this;b.removeAttribute("style"),b.style.display="block";var e={data:c,colHeaders:!1,colWidths:[200],readOnly:!0,copyPaste:!1,columns:[{data:"name",renderer:a.helper.proxy(this.renderer,this)}],renderAllRows:!0,beforeKeyDown:function(a){d.onBeforeKeyDown(a,f)},afterOnCellMouseOver:function(a,b,c){d.onCellMouseOver(a,b,c,f)}},f=new a(b,e);this.eventManager.removeEventListener(b,"mousedown"),this.eventManager.addEventListener(b,"mousedown",function(a){d.performAction(a,f)}),this.bindTableEvents(),f.listen(),this.htMenus[f.guid]=f},f.prototype.close=function(a){this.hide(a),this.eventManager.clear(),this.unbindTableEvents(),this.instance.listen()},f.prototype.closeAll=function(){for(;this.menus.length>0;){var a=this.menus.pop();a&&this.close(a)}this.triggerRows=[]},f.prototype.closeLastOpenedSubMenu=function(){var a=this.menus.pop();a&&this.hide(a)},f.prototype.hide=function(a){a.style.display="none";var b=this.htMenus[a.id];b.destroy(),delete this.htMenus[a.id]},f.prototype.renderer=function(b,c,d,e,g,h){function i(a){return a.hasOwnProperty("submenu")}function j(a){return new RegExp(f.SEPARATOR.name,"i").test(a.name)}function k(a){return a.disabled===!0||"function"==typeof a.disabled&&a.disabled.call(l.instance)===!0}var l=this,m=b.getData()[d],n=document.createElement("DIV");"function"==typeof h&&(h=h.call(this.instance)),a.Dom.empty(c),c.appendChild(n),j(m)?a.Dom.addClass(c,"htSeparator"):a.Dom.fastInnerHTML(n,h),k(m)?(a.Dom.addClass(c,"htDisabled"),this.eventManager.addEventListener(n,"mouseenter",function(){b.deselectCell()})):i(m)?(a.Dom.addClass(c,"htSubmenu"),this.eventManager.addEventListener(n,"mouseenter",function(){b.selectCell(d,e)})):(a.Dom.removeClass(c,"htSubmenu"),a.Dom.removeClass(c,"htDisabled"),this.eventManager.addEventListener(n,"mouseenter",function(){b.selectCell(d,e)}))},f.prototype.onCellMouseOver=function(a,b,c,d){var e=this.menus.length;if(e>0){var f=this.menus[e-1];f.id!=d.guid&&this.closeLastOpenedSubMenu()}else this.closeLastOpenedSubMenu();if(-1!=c.className.indexOf("htSubmenu")){var g=d.getData()[b.row],h=this.getItems(g.submenu),i=this.createMenu(g.name,b.row),j=c.getBoundingClientRect();this.show(i,h),this.setSubMenuPosition(j,i)}},f.prototype.onBeforeKeyDown=function(b,c){function d(a){var b=a.getCell(0,0);f.utils.isSeparator(b)||f.utils.isDisabled(b)?g(0,0,a):a.selectCell(0,0)}function e(a){var b=a.countRows()-1,c=a.getCell(b,0);f.utils.isSeparator(c)||f.utils.isDisabled(c)?h(b,0,a):a.selectCell(b,0)}function g(a,b,c){var d=a+1,e=d=0?c.getCell(d,b):null;e&&(f.utils.isSeparator(e)||f.utils.isDisabled(e)?h(d,b,c):c.selectCell(d,b))}function i(a,b,c,d){var e=a.getData()[d],f=b.getItems(e.submenu),g=b.createMenu(e.name,d),h=c.getBoundingClientRect(),i=b.show(g,f);b.setSubMenuPosition(h,g),i.selectCell(0,0)}a.Dom.enableImmediatePropagation(b);var j=this,k=c.getSelected();switch(b.keyCode){case a.helper.keyCode.ESCAPE:j.closeAll(),b.preventDefault(),b.stopImmediatePropagation();break;case a.helper.keyCode.ENTER:k&&j.performAction(b,c);break;case a.helper.keyCode.ARROW_DOWN:k?g(k[0],k[1],c,j):d(c,j),b.preventDefault(),b.stopImmediatePropagation();break;case a.helper.keyCode.ARROW_UP:k?h(k[0],k[1],c,j):e(c,j),b.preventDefault(),b.stopImmediatePropagation();break;case a.helper.keyCode.ARROW_RIGHT:if(k){var l=k[0],m=c.getCell(k[0],0);f.utils.hasSubMenu(m)&&i(c,j,m,l)}b.preventDefault(),b.stopImmediatePropagation();break;case a.helper.keyCode.ARROW_LEFT:if(k){if(-1!=c.rootElement.className.indexOf("htContextSubMenu_")){j.closeLastOpenedSubMenu();var n=j.menus.length;if(n>0){var o=j.menus[n-1],p=j.triggerRows.pop();c=this.htMenus[o.id],c.selectCell(p,0)}}b.preventDefault(),b.stopImmediatePropagation()}}},f.prototype.getItems=function(b){function c(b){"string"==typeof b?this.name=b:a.helper.extend(this,b)}var d,e;c.prototype=b,b&&b.items&&(b=b.items),b===!0&&(b=this.defaultOptions.items),d=[];for(var f in b)b.hasOwnProperty(f)&&(e="string"==typeof b[f]?g(this.defaultOptions.items,b[f]):g(this.defaultOptions.items,f),e||(e=b[f]),e=new c(e),"object"==typeof b[f]&&a.helper.extend(e,b[f]),e.key||(e.key=f),d.push(e));return d},f.prototype.setSubMenuPosition=function(b,c){var d=a.Dom.getWindowScrollTop(),e=a.Dom.getWindowScrollLeft(),f={top:d+b.top,topRelative:b.top,left:b.left,leftRelative:b.left-e,scrollTop:d,scrollLeft:e,cellHeight:b.height,cellWidth:b.width};this.menuFitsBelowCursor(f,c,document.body.clientWidth)?this.positionMenuBelowCursor(f,c,!0):this.menuFitsAboveCursor(f,c)?this.positionMenuAboveCursor(f,c,!0):this.positionMenuBelowCursor(f,c,!0),this.menuFitsOnRightOfCursor(f,c,document.body.clientWidth)?this.positionMenuOnRightOfCursor(f,c,!0):this.positionMenuOnLeftOfCursor(f,c,!0)},f.prototype.setMenuPosition=function(b,c){var d=a.Dom.getWindowScrollTop(),e=a.Dom.getWindowScrollLeft(),f=b.pageY||b.clientY+d,g=b.pageX||b.clientX+e,h={top:f,topRelative:f-d,left:g,leftRelative:g-e,scrollTop:d,scrollLeft:e,cellHeight:b.target.clientHeight,cellWidth:b.target.clientWidth};this.menuFitsBelowCursor(h,c,document.body.clientHeight)?this.positionMenuBelowCursor(h,c):this.menuFitsAboveCursor(h,c)?this.positionMenuAboveCursor(h,c):this.positionMenuBelowCursor(h,c),this.menuFitsOnRightOfCursor(h,c,document.body.clientWidth)?this.positionMenuOnRightOfCursor(h,c):this.positionMenuOnLeftOfCursor(h,c)},f.prototype.menuFitsAboveCursor=function(a,b){return a.topRelative>=b.offsetHeight},f.prototype.menuFitsBelowCursor=function(a,b,c){return a.topRelative+b.offsetHeight<=c},f.prototype.menuFitsOnRightOfCursor=function(a,b,c){return a.leftRelative+b.offsetWidth<=c},f.prototype.positionMenuBelowCursor=function(a,b){b.style.top=a.top+"px"},f.prototype.positionMenuAboveCursor=function(a,b,c){b.style.top=c?a.top+a.cellHeight-b.offsetHeight+"px":a.top-b.offsetHeight+"px"},f.prototype.positionMenuOnRightOfCursor=function(a,b,c){b.style.left=c?1+a.left+a.cellWidth+"px":1+a.left+"px"},f.prototype.positionMenuOnLeftOfCursor=function(a,b,c){b.style.left=c?a.left-b.offsetWidth+"px":a.left-b.offsetWidth+"px"},f.utils={},f.utils.normalizeSelection=function(a){return{start:a.getTopLeftCorner(),end:a.getBottomRightCorner()}},f.utils.isSeparator=function(b){return a.Dom.hasClass(b,"htSeparator")},f.utils.hasSubMenu=function(b){return a.Dom.hasClass(b,"htSubmenu")},f.utils.isDisabled=function(b){return a.Dom.hasClass(b,"htDisabled")},f.prototype.enable=function(){this.enabled||(this.enabled=!0,this.bindMouseEvents())},f.prototype.disable=function(){this.enabled&&(this.enabled=!1,this.closeAll(),this.unbindMouseEvents(),this.unbindTableEvents())},f.prototype.destroy=function(){for(this.closeAll();this.menus.length>0;){var a=this.menus.pop();this.triggerRows.pop(),a&&(this.close(a),this.isMenuEnabledByOtherHotInstance()||this.removeMenu(a))}this.unbindMouseEvents(),this.unbindTableEvents()},f.prototype.isMenuEnabledByOtherHotInstance=function(){for(var a=document.querySelectorAll(".handsontable"),b=!1,c=0,d=a.length;d>c;c++){var e=this.htMenus[a[c].id];if(e&&e.getSettings().contextMenu){b=!0;break}}return b},f.prototype.removeMenu=function(a){a.parentNode&&this.menu.parentNode.removeChild(a)},f.SEPARATOR={name:"---------"},a.hooks.add("afterInit",j),a.hooks.add("afterUpdateSettings",j),a.hooks.add("afterInit",i),a.PluginHooks.register("afterContextMenuDefaultOptions"),a.ContextMenu=f}(b);var M=function(){var a=this,c=a.getSettings().comments;c&&(b.Comments=new e(a),b.Comments.init())},N=function(a,c,d,e,f,g){g.comment&&b.Dom.addClass(a,g.commentedCellClassName)},O=function(a){var c=this;c.getSettings().comments&&(a.items.push(b.ContextMenu.SEPARATOR),a.items.push({key:"commentsAddEdit",name:function(){var a=b.Comments.checkSelectionCommentsConsistency();return a?"Edit Comment":"Add Comment"},callback:function(){b.Comments.showComment(this.getSelectedRange())},disabled:function(){return!1}}),a.items.push({key:"commentsRemove",name:function(){return"Delete Comment"},callback:function(a,c){b.Comments.removeComment(c.start.row,c.start.col)},disabled:function(){var a=b.Comments.checkSelectionCommentsConsistency();return!a}}))};b.hooks.add("beforeInit",M),b.hooks.add("afterContextMenuDefaultOptions",O),b.hooks.add("afterRenderer",N),function(b){function c(){function c(a){m=this,n=a;var b=this.view.wt.wtTable.getCoords(a).col;if(b>=0){l=b;var c=n.getBoundingClientRect();k=c.left,o.style.top=c.top+"px",o.style.left=k+"px",m.rootElement.appendChild(o)}}function d(a,b){var c=a.getBoundingClientRect(),d=6;o.style.left=b>0?c.left+c.width-d+"px":c.left+"px"}function e(){var a=this;b.Dom.addClass(o,"active"),b.Dom.addClass(p,"active");var c=n.getBoundingClientRect();p.style.width=c.width+"px",p.style.height=a.view.maximumVisibleElementHeight(0)+"px",p.style.top=o.style.top,p.style.left=k+"px",a.rootElement.appendChild(p)}function f(a){p.style.left=k+a+"px"}function g(){b.Dom.removeClass(o,"active"),b.Dom.removeClass(p,"active")}var h,i,j,k,l,m,n,o=document.createElement("DIV"),p=document.createElement("DIV"),q=b.eventManager(this);o.className="manualColumnMover",p.className="manualColumnMoverGuide";var r=function(){var a=this;b.hooks.run(a,"persistentStateSave","manualColumnPositions",a.manualColumnPositions)},s=function(){var a=this,c={};return b.hooks.run(a,"persistentStateLoad","manualColumnPositions",c),c.value},t=function(a){return"BODY"!=a.tagName?"THEAD"==a.parentNode.tagName?!0:(a=a.parentNode,t(a)):!1},u=function(a){return"TABLE"!=a.tagName?"TH"==a.tagName?a:u(a.parentNode):null},v=function(){var k,m=this;q.addEventListener(m.rootElement,"mouseover",function(a){if(t(a.target)){var b=u(a.target);if(b)if(k){var e=m.view.wt.wtTable.getCoords(b).col;e>=0&&(i=e,d(a.target,i-h))}else c.call(m,b)}}),q.addEventListener(m.rootElement,"mousedown",function(a){b.Dom.hasClass(a.target,"manualColumnMover")&&(j=b.helper.pageX(a),e.call(m),k=m,h=l,i=l)}),q.addEventListener(a,"mousemove",function(a){k&&f(b.helper.pageX(a)-j)}),q.addEventListener(a,"mouseup",function(){k&&(g(),k=!1,x(m.manualColumnPositions,m.countCols()),m.manualColumnPositions.splice(i,0,m.manualColumnPositions.splice(h,1)[0]),m.forceFullRender=!0,m.view.render(),r.call(m),b.hooks.run(m,"afterColumnMove",h,i),c.call(m,n))}),m.addHook("afterDestroy",w)},w=function(){q.clear()},x=function(a,b){if(a.lengthc;c++)a[c]=c};this.beforeInit=function(){this.manualColumnPositions=[]},this.init=function(a){var b=this,c=!!this.getSettings().manualColumnMove;if(c){var d=this.getSettings().manualColumnMove,e=s.call(b);this.manualColumnPositions="undefined"!=typeof e?e:Array.isArray(d)?d:[],"afterInit"==a&&("undefined"!=typeof b.manualColumnPositionsPluginUsages?b.manualColumnPositionsPluginUsages.push("manualColumnMove"):b.manualColumnPositionsPluginUsages=["manualColumnMove"],v.call(this),this.manualColumnPositions.length>0&&(this.forceFullRender=!0,this.render()))}else{var f=b.manualColumnPositionsPluginUsages?b.manualColumnPositionsPluginUsages.indexOf("manualColumnMove"):-1;f>-1&&(w.call(this),this.manualColumnPositions=[],b.manualColumnPositionsPluginUsages[f]=void 0)}},this.modifyCol=function(a){return this.getSettings().manualColumnMove?("undefined"==typeof this.manualColumnPositions[a]&&x(this.manualColumnPositions,a+1),this.manualColumnPositions[a]):a},this.afterRemoveCol=function(a,b){if(this.getSettings().manualColumnMove){var c,d=this.manualColumnPositions;c=d.splice(a,b),d=d.map(function(a){var b,d=a;for(b=0;bc[b]&&d--;return d}),this.manualColumnPositions=d}},this.afterCreateCol=function(a,b){if(this.getSettings().manualColumnMove){var c=this.manualColumnPositions;if(c.length){for(var d=[],e=0;b>e;e++)d.push(a+e);a>=c.length?c.concat(d):(c=c.map(function(c){return c>=a?c+b:c}),c.splice.apply(c,[a,0].concat(d))),this.manualColumnPositions=c}}}}var d=new c;b.hooks.add("beforeInit",d.beforeInit),b.hooks.add("afterInit",function(){d.init.call(this,"afterInit")}),b.hooks.add("afterUpdateSettings",function(){d.init.call(this,"afterUpdateSettings")}),b.hooks.add("modifyCol",d.modifyCol),b.hooks.add("afterRemoveCol",d.afterRemoveCol),b.hooks.add("afterCreateCol",d.afterCreateCol),b.hooks.register("afterColumnMove")}(b),function(b){function c(){function c(a){k=this,h=a;var b=this.view.wt.wtTable.getCoords(a).col;if(b>=0){i=b;var c=h.getBoundingClientRect();o=c.left-6,n=parseInt(c.width,10),p.style.top=c.top+"px",p.style.left=o+n+"px",k.rootElement.appendChild(p)}}function d(){p.style.left=o+j+"px"}function e(){var a=this;b.Dom.addClass(p,"active"),b.Dom.addClass(q,"active"),q.style.top=p.style.top,q.style.left=p.style.left,q.style.height=a.view.maximumVisibleElementHeight(0)+"px",a.rootElement.appendChild(q)}function f(){q.style.left=p.style.left}function g(){b.Dom.removeClass(p,"active"),b.Dom.removeClass(q,"active")}var h,i,j,k,l,m,n,o,p=document.createElement("DIV"),q=document.createElement("DIV"),r=b.eventManager(this);p.className="manualColumnResizer",q.className="manualColumnResizerGuide";var s=function(){var a=this;b.hooks.run(a,"persistentStateSave","manualColumnWidths",a.manualColumnWidths)},t=function(){var a=this,c={};return b.hooks.run(a,"persistentStateLoad","manualColumnWidths",c),c.value},u=function(a){return"BODY"!=a.tagName?"THEAD"==a.parentNode.tagName?!0:(a=a.parentNode,u(a)):!1},v=function(a){return"TABLE"!=a.tagName?"TH"==a.tagName?a:v(a.parentNode):null +},w=function(){var k,o=this,p=0,q=null;r.addEventListener(o.rootElement,"mouseover",function(a){if(u(a.target)){var b=v(a.target);b&&(k||c.call(o,b))}}),r.addEventListener(o.rootElement,"mousedown",function(a){b.Dom.hasClass(a.target,"manualColumnResizer")&&(e.call(o),k=o,null==q&&(q=setTimeout(function(){p>=2&&(l=o.determineColumnWidth.call(o,i),y(i,l),o.forceFullRender=!0,o.view.render(),b.hooks.run(o,"afterColumnResize",i,l)),p=0,q=null},500),o._registerTimeout(q)),p++,m=b.helper.pageX(a),l=n)}),r.addEventListener(a,"mousemove",function(a){k&&(j=n+(b.helper.pageX(a)-m),l=y(i,j),d(),f())}),r.addEventListener(a,"mouseup",function(){k&&(g(),k=!1,l!=n&&(o.forceFullRender=!0,o.view.render(),s.call(o),b.hooks.run(o,"afterColumnResize",i,l)),c.call(o,h))}),o.addHook("afterDestroy",x)},x=function(){r.clear()};this.beforeInit=function(){this.manualColumnWidths=[]},this.init=function(a){var b=this,c=!!this.getSettings().manualColumnResize;if(c){var d=this.getSettings().manualColumnResize,e=t.call(b);"undefined"!=typeof b.manualColumnWidthsPluginUsages?b.manualColumnWidthsPluginUsages.push("manualColumnResize"):b.manualColumnWidthsPluginUsages=["manualColumnResize"],this.manualColumnWidths="undefined"!=typeof e?e:Array.isArray(d)?d:[],"afterInit"==a&&(w.call(this),this.manualColumnWidths.length>0&&(this.forceFullRender=!0,this.render()))}else{var f=b.manualColumnWidthsPluginUsages?b.manualColumnWidthsPluginUsages.indexOf("manualColumnResize"):-1;f>-1&&(x.call(this),this.manualColumnWidths=[])}};var y=function(a,c){return c=Math.max(c,20),a=b.hooks.run(k,"modifyCol",a),k.manualColumnWidths[a]=c,c};this.modifyColWidth=function(a,b){return b=this.runHooks("modifyCol",b),this.getSettings().manualColumnResize&&this.manualColumnWidths[b]?this.manualColumnWidths[b]:a}}var d=new c;b.hooks.add("beforeInit",d.beforeInit),b.hooks.add("afterInit",function(){d.init.call(this,"afterInit")}),b.hooks.add("afterUpdateSettings",function(){d.init.call(this,"afterUpdateSettings")}),b.hooks.add("modifyColWidth",d.modifyColWidth),b.hooks.register("afterColumnResize")}(b),function(b){function c(){function c(a){k=this,h=a;var b=this.view.wt.wtTable.getCoords(a).row;if(b>=0){i=b;var c=h.getBoundingClientRect();o=c.top-6,n=parseInt(c.height,10),p.style.left=c.left+"px",p.style.top=o+n+"px",k.rootElement.appendChild(p)}}function d(){p.style.top=o+j+"px"}function e(){var a=this;b.Dom.addClass(p,"active"),b.Dom.addClass(q,"active"),q.style.top=p.style.top,q.style.left=p.style.left,q.style.width=a.view.maximumVisibleElementWidth(0)+"px",a.rootElement.appendChild(q)}function f(){q.style.top=p.style.top}function g(){b.Dom.removeClass(p,"active"),b.Dom.removeClass(q,"active")}var h,i,j,k,l,m,n,o,p=document.createElement("DIV"),q=document.createElement("DIV"),r=b.eventManager(this);p.className="manualRowResizer",q.className="manualRowResizerGuide";var s=function(){var a=this;b.hooks.run(a,"persistentStateSave","manualRowHeights",a.manualRowHeights)},t=function(){var a=this,c={};return b.hooks.run(a,"persistentStateLoad","manualRowHeights",c),c.value},u=function(a){return"BODY"!=a.tagName?"TBODY"==a.parentNode.tagName?!0:(a=a.parentNode,u(a)):!1},v=function(a){return"TABLE"!=a.tagName?"TH"==a.tagName?a:v(a.parentNode):null},w=function(){var k,o=this,p=0,q=null;r.addEventListener(o.rootElement,"mouseover",function(a){if(u(a.target)){var b=v(a.target);b&&(k||c.call(o,b))}}),r.addEventListener(o.rootElement,"mousedown",function(a){b.Dom.hasClass(a.target,"manualRowResizer")&&(e.call(o),k=o,null==q&&(q=setTimeout(function(){p>=2&&(y(i,null),o.forceFullRender=!0,o.view.render(),b.hooks.run(o,"afterRowResize",i,l)),p=0,q=null},500),o._registerTimeout(q)),p++,m=b.helper.pageY(a),l=n)}),r.addEventListener(a,"mousemove",function(a){k&&(j=n+(b.helper.pageY(a)-m),l=y(i,j),d(),f())}),r.addEventListener(a,"mouseup",function(){k&&(g(),k=!1,l!=n&&(o.forceFullRender=!0,o.view.render(),s.call(o),b.hooks.run(o,"afterRowResize",i,l)),c.call(o,h))}),o.addHook("afterDestroy",x)},x=function(){r.clear()};this.beforeInit=function(){this.manualRowHeights=[]},this.init=function(a){var b=this,c=!!this.getSettings().manualRowResize;if(c){var d=this.getSettings().manualRowResize,e=t.call(b);"undefined"!=typeof b.manualRowHeightsPluginUsages?b.manualRowHeightsPluginUsages.push("manualRowResize"):b.manualRowHeightsPluginUsages=["manualRowResize"],this.manualRowHeights="undefined"!=typeof e?e:Array.isArray(d)?d:[],"afterInit"===a?(w.call(this),this.manualRowHeights.length>0&&(this.forceFullRender=!0,this.render())):(this.forceFullRender=!0,this.render())}else{var f=b.manualRowHeightsPluginUsages?b.manualRowHeightsPluginUsages.indexOf("manualRowResize"):-1;f>-1&&(x.call(this),this.manualRowHeights=[],b.manualRowHeightsPluginUsages[f]=void 0)}};var y=function(a,c){return a=b.hooks.run(k,"modifyRow",a),k.manualRowHeights[a]=c,c};this.modifyRowHeight=function(a,b){return this.getSettings().manualRowResize&&(b=this.runHooks("modifyRow",b),void 0!==this.manualRowHeights[b])?this.manualRowHeights[b]:a}}var d=new c;b.hooks.add("beforeInit",d.beforeInit),b.hooks.add("afterInit",function(){d.init.call(this,"afterInit")}),b.hooks.add("afterUpdateSettings",function(){d.init.call(this,"afterUpdateSettings")}),b.hooks.add("modifyRowHeight",d.modifyRowHeight),b.hooks.register("afterRowResize")}(b),function(){function a(){var a=this,b=a.getSettings().observeChanges;b?(a.observer&&e.call(a),c.call(a),g.call(a)):b||e.call(a)}function c(){var a=this;a.observeChangesActive=!0,a.pauseObservingChanges=function(){a.observeChangesActive=!1},a.resumeObservingChanges=function(){a.observeChangesActive=!0},a.observedData=a.getData(),a.observer=K.observe(a.observedData,function(b){a.observeChangesActive&&(d.call(a,b),a.render()),a.runHooks("afterChangesObserved")})}function d(a){function b(a){var b;return b=d(a),b=c(b)}function c(a){var b=[];return a.filter(function(a){var c=e(a.path);if(-1!=["add","remove"].indexOf(a.op)&&!isNaN(c.col)){if(-1!=b.indexOf(c.col))return!1;b.push(c.col)}return!0})}function d(a){return a.filter(function(a){return!/[/]length/gi.test(a.path)})}function e(a){var b=a.match(/^\/(\d+)\/?(.*)?$/);return{row:parseInt(b[1],10),col:/^\d*$/.test(b[2])?parseInt(b[2],10):b[2]}}for(var f=this,g=b(a),h=0,i=g.length;i>h;h++){var j=g[h],k=e(j.path);switch(j.op){case"add":isNaN(k.col)?f.runHooks("afterCreateRow",k.row):f.runHooks("afterCreateCol",k.col);break;case"remove":isNaN(k.col)?f.runHooks("afterRemoveRow",k.row,1):f.runHooks("afterRemoveCol",k.col,1);break;case"replace":f.runHooks("afterChange",[k.row,k.col,null,j.value],"external")}}}function e(){var a=this;a.observer&&(f.call(a),h.call(a))}function f(){var a=this;K.unobserve(a.observedData,a.observer),delete a.observeChangesActive,delete a.pauseObservingChanges,delete a.resumeObservingChanges}function g(){var a=this;a.addHook("afterDestroy",e),a.addHook("afterCreateRow",i),a.addHook("afterRemoveRow",i),a.addHook("afterCreateCol",i),a.addHook("afterRemoveCol",i),a.addHook("afterChange",function(a,b){"loadData"!=b&&i.call(this)})}function h(){var a=this;a.removeHook("afterDestroy",e),a.removeHook("afterCreateRow",i),a.removeHook("afterRemoveRow",i),a.removeHook("afterCreateCol",i),a.removeHook("afterRemoveCol",i),a.removeHook("afterChange",i)}function i(){var a=this;a.pauseObservingChanges(),a.addHookOnce("afterChangesObserved",function(){a.resumeObservingChanges()})}b.hooks.add("afterLoadData",a),b.hooks.add("afterUpdateSettings",a),b.hooks.register("afterChangesObserved")}(),function(a){function c(){function c(){var a=this;for(var b in f)f.hasOwnProperty(b)&&a.addHook(b,f[b])}function d(){var a=this;for(var b in f)f.hasOwnProperty(b)&&a.removeHook(b,f[b])}var e=this;this.init=function(){var b=this,f=b.getSettings().persistentState;return e.enabled=!!f,e.enabled?(b.storage||(b.storage=new a(b.rootElement.id)),b.resetState=e.resetValue,void c.call(b)):void d.call(b)},this.saveValue=function(a,b){var c=this;c.storage.saveValue(a,b)},this.loadValue=function(a,b){var c=this;b.value=c.storage.loadValue(a)},this.resetValue=function(a){var b=this;"undefined"!=typeof a?b.storage.reset(a):b.storage.resetAll()};var f={persistentStateSave:e.saveValue,persistentStateLoad:e.loadValue,persistentStateReset:e.resetValue};for(var g in f)f.hasOwnProperty(g)&&b.hooks.register(g)}var d=new c;b.hooks.add("beforeInit",d.init),b.hooks.add("afterUpdateSettings",d.init)}(f),function(a){a.UndoRedo=function(b){var c=this;this.instance=b,this.doneActions=[],this.undoneActions=[],this.ignoreNewActions=!1,b.addHook("afterChange",function(b){if(b){var d=new a.UndoRedo.ChangeAction(b);c.done(d)}}),b.addHook("afterCreateRow",function(b,d,e){if(!e){var f=new a.UndoRedo.CreateRowAction(b,d);c.done(f)}}),b.addHook("beforeRemoveRow",function(b,d){var e=c.instance.getData();b=(e.length+b)%e.length;var f=e.slice(b,b+d),g=new a.UndoRedo.RemoveRowAction(b,f);c.done(g)}),b.addHook("afterCreateCol",function(b,d,e){if(!e){var f=new a.UndoRedo.CreateColumnAction(b,d);c.done(f)}}),b.addHook("beforeRemoveCol",function(d,e){var f=c.instance.getData();d=(c.instance.countCols()+d)%c.instance.countCols();for(var g=[],h=0,i=f.length;i>h;h++)g[h]=f[h].slice(d,d+e);var j;Array.isArray(b.getSettings().colHeaders)&&(j=b.getSettings().colHeaders.slice(d,d+g.length));var k=new a.UndoRedo.RemoveColumnAction(d,g,j);c.done(k)})},a.UndoRedo.prototype.done=function(a){this.ignoreNewActions||(this.doneActions.push(a),this.undoneActions.length=0)},a.UndoRedo.prototype.undo=function(){if(this.isUndoAvailable()){var a=this.doneActions.pop();this.ignoreNewActions=!0;var b=this;a.undo(this.instance,function(){b.ignoreNewActions=!1,b.undoneActions.push(a)})}},a.UndoRedo.prototype.redo=function(){if(this.isRedoAvailable()){var a=this.undoneActions.pop();this.ignoreNewActions=!0;var b=this;a.redo(this.instance,function(){b.ignoreNewActions=!1,b.doneActions.push(a)})}},a.UndoRedo.prototype.isUndoAvailable=function(){return this.doneActions.length>0},a.UndoRedo.prototype.isRedoAvailable=function(){return this.undoneActions.length>0},a.UndoRedo.prototype.clear=function(){this.doneActions.length=0,this.undoneActions.length=0},a.UndoRedo.Action=function(){},a.UndoRedo.Action.prototype.undo=function(){},a.UndoRedo.Action.prototype.redo=function(){},a.UndoRedo.ChangeAction=function(a){this.changes=a},a.helper.inherit(a.UndoRedo.ChangeAction,a.UndoRedo.Action),a.UndoRedo.ChangeAction.prototype.undo=function(b,c){for(var d=a.helper.deepClone(this.changes),e=b.countEmptyRows(!0),f=b.countEmptyCols(!0),g=0,h=d.length;h>g;g++)d[g].splice(3,1);b.addHookOnce("afterChange",c),b.setDataAtRowProp(d,null,null,"undo");for(var g=0,h=d.length;h>g;g++)b.getSettings().minSpareRows&&d[g][0]+1+b.getSettings().minSpareRows===b.countRows()&&e==b.getSettings().minSpareRows&&(b.alter("remove_row",parseInt(d[g][0]+1,10),b.getSettings().minSpareRows),b.undoRedo.doneActions.pop()),b.getSettings().minSpareCols&&d[g][1]+1+b.getSettings().minSpareCols===b.countCols()&&f==b.getSettings().minSpareCols&&(b.alter("remove_col",parseInt(d[g][1]+1,10),b.getSettings().minSpareCols),b.undoRedo.doneActions.pop())},a.UndoRedo.ChangeAction.prototype.redo=function(b,c){for(var d=a.helper.deepClone(this.changes),e=0,f=d.length;f>e;e++)d[e].splice(2,1);b.addHookOnce("afterChange",c),b.setDataAtRowProp(d,null,null,"redo")},a.UndoRedo.CreateRowAction=function(a,b){this.index=a,this.amount=b},a.helper.inherit(a.UndoRedo.CreateRowAction,a.UndoRedo.Action),a.UndoRedo.CreateRowAction.prototype.undo=function(a,b){a.addHookOnce("afterRemoveRow",b),a.alter("remove_row",this.index,this.amount)},a.UndoRedo.CreateRowAction.prototype.redo=function(a,b){a.addHookOnce("afterCreateRow",b),a.alter("insert_row",this.index+1,this.amount)},a.UndoRedo.RemoveRowAction=function(a,b){this.index=a,this.data=b},a.helper.inherit(a.UndoRedo.RemoveRowAction,a.UndoRedo.Action),a.UndoRedo.RemoveRowAction.prototype.undo=function(a,b){var c=[this.index,0];Array.prototype.push.apply(c,this.data),Array.prototype.splice.apply(a.getData(),c),a.addHookOnce("afterRender",b),a.render()},a.UndoRedo.RemoveRowAction.prototype.redo=function(a,b){a.addHookOnce("afterRemoveRow",b),a.alter("remove_row",this.index,this.data.length)},a.UndoRedo.CreateColumnAction=function(a,b){this.index=a,this.amount=b},a.helper.inherit(a.UndoRedo.CreateColumnAction,a.UndoRedo.Action),a.UndoRedo.CreateColumnAction.prototype.undo=function(a,b){a.addHookOnce("afterRemoveCol",b),a.alter("remove_col",this.index,this.amount)},a.UndoRedo.CreateColumnAction.prototype.redo=function(a,b){a.addHookOnce("afterCreateCol",b),a.alter("insert_col",this.index+1,this.amount)},a.UndoRedo.RemoveColumnAction=function(a,b,c){this.index=a,this.data=b,this.amount=this.data[0].length,this.headers=c},a.helper.inherit(a.UndoRedo.RemoveColumnAction,a.UndoRedo.Action),a.UndoRedo.RemoveColumnAction.prototype.undo=function(a,b){for(var c,d,e=0,f=a.getData().length;f>e;e++)c=a.getSourceDataAtRow(e),d=[this.index,0],Array.prototype.push.apply(d,this.data[e]),Array.prototype.splice.apply(c,d);"undefined"!=typeof this.headers&&(d=[this.index,0],Array.prototype.push.apply(d,this.headers),Array.prototype.splice.apply(a.getSettings().colHeaders,d)),a.addHookOnce("afterRender",b),a.render()},a.UndoRedo.RemoveColumnAction.prototype.redo=function(a,b){a.addHookOnce("afterRemoveCol",b),a.alter("remove_col",this.index,this.amount)}}(b),function(a){function b(){var b=this,g="undefined"==typeof b.getSettings().undo||b.getSettings().undo;g?b.undoRedo||(b.undoRedo=new a.UndoRedo(b),e(b),b.addHook("beforeKeyDown",c),b.addHook("afterChange",d)):b.undoRedo&&(delete b.undoRedo,f(b),b.removeHook("beforeKeyDown",c),b.removeHook("afterChange",d))}function c(a){var b=this,c=(a.ctrlKey||a.metaKey)&&!a.altKey;c&&(89===a.keyCode||a.shiftKey&&90===a.keyCode?(b.undoRedo.redo(),a.stopImmediatePropagation()):90===a.keyCode&&(b.undoRedo.undo(),a.stopImmediatePropagation()))}function d(a,b){var c=this;return"loadData"==b?c.undoRedo.clear():void 0}function e(a){a.undo=function(){return a.undoRedo.undo()},a.redo=function(){return a.undoRedo.redo()},a.isUndoAvailable=function(){return a.undoRedo.isUndoAvailable()},a.isRedoAvailable=function(){return a.undoRedo.isRedoAvailable()},a.clearUndo=function(){return a.undoRedo.clear()}}function f(a){delete a.undo,delete a.redo,delete a.isUndoAvailable,delete a.isRedoAvailable,delete a.clearUndo}a.hooks.add("afterInit",b),a.hooks.add("afterUpdateSettings",b)}(b),g.prototype.setBoundaries=function(a){this.boundaries=a},g.prototype.setCallback=function(a){this.callback=a},g.prototype.check=function(a,b){var c=0,d=0;bthis.boundaries.bottom&&(d=b-this.boundaries.bottom),athis.boundaries.right&&(c=a-this.boundaries.right),this.callback(c,d)};var P,Q;if("undefined"!=typeof b){var R=function(b){b.dragToScrollListening=!1;var c=b.view.wt.wtScrollbars.vertical.scrollHandler;P=new g,c!==a&&(P.setBoundaries(c.getBoundingClientRect()),P.setCallback(function(a,b){0>a?c.scrollLeft-=50:a>0&&(c.scrollLeft+=50),0>b?c.scrollTop-=20:b>0&&(c.scrollTop+=20)}),b.dragToScrollListening=!0)};b.hooks.add("afterInit",function(){var a=this,c=b.eventManager(this);c.addEventListener(document,"mouseup",function(){a.dragToScrollListening=!1}),c.addEventListener(document,"mousemove",function(b){a.dragToScrollListening&&P.check(b.clientX,b.clientY)})}),b.hooks.add("afterDestroy",function(){var a=b.eventManager(this);a.clear()}),b.hooks.add("afterOnCellMouseDown",function(){R(this)}),b.hooks.add("afterOnCellCornerMouseDown",function(){R(this)}),b.plugins.DragToScroll=g}if(function(a,b,c){function d(d){function e(){d.isListening()&&d.selection.empty()}function f(a){var b,e,f,g,h,i,j,k,n,o;d.isListening()&&d.selection.isSelected()&&(b=a,e=c.parse(b),f=d.getSelected(),g=new l(f[0],f[1]),h=new l(f[2],f[3]),i=new m(g,g,h),j=i.getTopLeftCorner(),k=i.getBottomRightCorner(),n=j,o=new l(Math.max(k.row,e.length-1+j.row),Math.max(k.col,e[0].length-1+j.col)),d.addHookOnce("afterChange",function(a){a&&a.length&&this.selectCell(n.row,n.col,o.row,o.col)}),d.populateFromArray(n.row,n.col,e,o.row,o.col,"paste",d.getSettings().pasteMode))}function g(b){var c;if(d.getSelected()){if(a.helper.isCtrlKey(b.keyCode))return h.setCopyableText(),void b.stopImmediatePropagation();c=(b.ctrlKey||b.metaKey)&&!b.altKey,b.keyCode==a.helper.keyCode.A&&c&&d._registerTimeout(setTimeout(a.helper.proxy(h.setCopyableText,h),0))}}var h=this;this.copyPasteInstance=b.getInstance(),this.copyPasteInstance.onCut(e),this.copyPasteInstance.onPaste(f),d.addHook("beforeKeyDown",g),this.destroy=function(){this.copyPasteInstance.removeCallback(e),this.copyPasteInstance.removeCallback(f),this.copyPasteInstance.destroy(),d.removeHook("beforeKeyDown",g)},d.addHook("afterDestroy",a.helper.proxy(this.destroy,this)),this.triggerPaste=a.helper.proxy(this.copyPasteInstance.triggerPaste,this.copyPasteInstance),this.triggerCut=a.helper.proxy(this.copyPasteInstance.triggerCut,this.copyPasteInstance),this.setCopyableText=function(){var b=d.getSettings(),c=b.copyRowsLimit,e=b.copyColsLimit,f=d.getSelectedRange(),g=f.getTopLeftCorner(),h=f.getBottomRightCorner(),i=g.row,j=g.col,k=h.row,l=h.col,m=Math.min(k,i+c-1),n=Math.min(l,j+e-1);d.copyPaste.copyPasteInstance.copyable(d.getCopyableData(i,j,m,n)),(k!==m||l!==n)&&a.hooks.run(d,"afterCopyLimit",k-i+1,l-j+1,c,e)}}function e(){var a=this,b=a.getSettings().copyPaste!==!1;b&&!a.copyPaste?a.copyPaste=new d(a):!b&&a.copyPaste&&(a.copyPaste.destroy(),delete a.copyPaste)}a.hooks.add("afterInit",e),a.hooks.add("afterUpdateSettings",e),a.hooks.register("afterCopyLimit")}(b,J,SheetClip),function(a){function b(){var b=this,c=!!b.getSettings().search;c?b.search=new a.Search(b):delete b.search}a.Search=function(b){this.query=function(c,d,e){var f=b.countRows(),g=b.countCols(),h=[];d||(d=a.Search.global.getDefaultCallback()),e||(e=a.Search.global.getDefaultQueryMethod());for(var i=0;f>i;i++)for(var j=0;g>j;j++){var k=b.getDataAtCell(i,j),l=b.getCellMeta(i,j),m=l.search.callback||d,n=l.search.queryMethod||e,o=n(c,k);if(o){var p={row:i,col:j,data:k};h.push(p)}m&&m(b,i,j,k,o)}return h}},a.Search.DEFAULT_CALLBACK=function(a,b,c,d,e){a.getCellMeta(b,c).isSearchResult=e},a.Search.DEFAULT_QUERY_METHOD=function(a,b){return"undefined"!=typeof a&&null!=a&&a.toLowerCase&&0!==a.length?"undefined"==typeof b||null==b?!1:-1!=b.toString().toLowerCase().indexOf(a.toLowerCase()):!1},a.Search.DEFAULT_SEARCH_RESULT_CLASS="htSearchResult",a.Search.global=function(){var b=a.Search.DEFAULT_CALLBACK,c=a.Search.DEFAULT_QUERY_METHOD,d=a.Search.DEFAULT_SEARCH_RESULT_CLASS;return{getDefaultCallback:function(){return b},setDefaultCallback:function(a){b=a},getDefaultQueryMethod:function(){return c},setDefaultQueryMethod:function(a){c=a},getDefaultSearchResultClass:function(){return d},setDefaultSearchResultClass:function(a){d=a}}}(),a.SearchCellDecorator=function(b,c,d,e,f,g,h){var i="object"==typeof h.search&&h.search.searchResultClass||a.Search.global.getDefaultSearchResultClass();h.isSearchResult?a.Dom.addClass(c,i):a.Dom.removeClass(c,i)};var c=a.renderers.cellDecorator;a.renderers.cellDecorator=function(){c.apply(this,arguments),a.SearchCellDecorator.apply(this,arguments)},a.hooks.add("afterInit",b),a.hooks.add("afterUpdateSettings",b)}(b),i.prototype.canMergeRange=function(a){return!a.isSingle()},i.prototype.mergeRange=function(a){if(this.canMergeRange(a)){var b=a.getTopLeftCorner(),c=a.getBottomRightCorner(),d={};d.row=b.row,d.col=b.col,d.rowspan=c.row-b.row+1,d.colspan=c.col-b.col+1,this.mergedCellInfoCollection.setInfo(d)}},i.prototype.mergeOrUnmergeSelection=function(a){var b=this.mergedCellInfoCollection.getInfo(a.from.row,a.from.col);b?this.unmergeSelection(a.from):this.mergeSelection(a)},i.prototype.mergeSelection=function(a){this.mergeRange(a)},i.prototype.unmergeSelection=function(a){var b=this.mergedCellInfoCollection.getInfo(a.row,a.col);this.mergedCellInfoCollection.removeInfo(b.row,b.col)},i.prototype.applySpanProperties=function(a,b,c){var d=this.mergedCellInfoCollection.getInfo(b,c);d?d.row===b&&d.col===c?(a.setAttribute("rowspan",d.rowspan),a.setAttribute("colspan",d.colspan)):(a.removeAttribute("rowspan"),a.removeAttribute("colspan"),a.style.display="none"):(a.removeAttribute("rowspan"),a.removeAttribute("colspan"))},i.prototype.modifyTransform=function(a,b,c){var d=function(a,b){return b.row>=a.row&&b.row<=a.row+a.rowspan-1?!0:!1},e=function(a,b){return b.col>=a.col&&b.col<=a.col+a.colspan-1?!0:!1},f=function(a){return new l(b.to.row+a.row,b.to.col+a.col)},g={row:c.row,col:c.col};if("modifyTransformStart"==a){this.lastDesiredCoords||(this.lastDesiredCoords=new l(null,null));for(var h,i=new l(b.highlight.row,b.highlight.col),j=this.mergedCellInfoCollection.getInfo(i.row,i.col),k=0,n=this.mergedCellInfoCollection.length;n>k;k++){var o=this.mergedCellInfoCollection[k];if(o=new l(o.row+o.rowspan-1,o.col+o.colspan-1),b.includes(o)){h=!0;break}}if(j){var p=new l(j.row,j.col),q=new l(j.row+j.rowspan-1,j.col+j.colspan-1),r=new m(p,p,q);r.includes(this.lastDesiredCoords)||(this.lastDesiredCoords=new l(null,null)),g.row=this.lastDesiredCoords.row?this.lastDesiredCoords.row-i.row:g.row,g.col=this.lastDesiredCoords.col?this.lastDesiredCoords.col-i.col:g.col,c.row>0?g.row=j.row+j.rowspan-1-i.row+c.row:c.row<0&&(g.row=i.row-j.row+c.row),c.col>0?g.col=j.col+j.colspan-1-i.col+c.col:c.col<0&&(g.col=i.col-j.col+c.col)}var s=new l(b.highlight.row+g.row,b.highlight.col+g.col),t=this.mergedCellInfoCollection.getInfo(s.row,s.col);t&&(this.lastDesiredCoords=s,g={row:t.row-i.row,col:t.col-i.col})}else if("modifyTransformEnd"==a)for(var k=0,n=this.mergedCellInfoCollection.length;n>k;k++){var u=this.mergedCellInfoCollection[k],p=new l(u.row,u.col),q=new l(u.row+u.rowspan-1,u.col+u.colspan-1),v=new m(p,p,q),w=b.getBordersSharedWith(v);if(v.isEqual(b))b.setDirection("NW-SE");else if(w.length>0){var x=b.highlight.isEqual(v.from);w.indexOf("top")>-1?b.to.isSouthEastOf(v.from)&&x?b.setDirection("NW-SE"):b.to.isSouthWestOf(v.from)&&x&&b.setDirection("NE-SW"):w.indexOf("bottom")>-1&&(b.to.isNorthEastOf(v.from)&&x?b.setDirection("SW-NE"):b.to.isNorthWestOf(v.from)&&x&&b.setDirection("SE-NW"))}var s=f(g),y=d(u,s),z=e(u,s);b.includesRange(v)&&(v.includes(s)||y||z)&&(y&&(g.row<0?g.row-=u.rowspan-1:g.row>0&&(g.row+=u.rowspan-1)),z&&(g.col<0?g.col-=u.colspan-1:g.col>0&&(g.col+=u.colspan-1)))}0!==g.row&&(c.row=g.row),0!==g.col&&(c.col=g.col)},"undefined"==typeof b)throw new Error("Handsontable is not defined");var S=function(){var a=this,b=a.getSettings().mergeCells;b&&(a.mergeCells||(a.mergeCells=new i(b)))},T=function(){var a=this;a.mergeCells&&(a.view.wt.wtTable.getCell=function(b){if(a.getSettings().mergeCells){var c=a.mergeCells.mergedCellInfoCollection.getInfo(b.row,b.col);c&&(b=c)}return B.prototype.getCell.call(this,b)})},U=function(a){if(this.mergeCells){var b=(a.ctrlKey||a.metaKey)&&!a.altKey;b&&77===a.keyCode&&(this.mergeCells.mergeOrUnmergeSelection(this.getSelectedRange()),this.render(),a.stopImmediatePropagation())}},V=function(a){this.getSettings().mergeCells&&(a.items.push(b.ContextMenu.SEPARATOR),a.items.push({key:"mergeCells",name:function(){var a=this.getSelected(),b=this.mergeCells.mergedCellInfoCollection.getInfo(a[0],a[1]);return b?"Unmerge cells":"Merge cells"},callback:function(){this.mergeCells.mergeOrUnmergeSelection(this.getSelectedRange()),this.render()},disabled:function(){return!1}}))},N=function(a,b,c){this.mergeCells&&this.mergeCells.applySpanProperties(a,b,c)},W=function(a){return function(b){var c=this.getSettings().mergeCells;if(c){var d=this.getSelectedRange();if(this.mergeCells.modifyTransform(a,d,b),"modifyTransformEnd"===a){var e=this.countRows(),f=this.countCols();d.from.row<0?d.from.row=0:d.from.row>0&&d.from.row>=e&&(d.from.row=d.from-1),d.from.col<0?d.from.col=0:d.from.col>0&&d.from.col>=f&&(d.from.col=f-1)}}}},X=function(a){this.lastDesiredCoords=null;var b=this.getSettings().mergeCells;if(b){var c=this.getSelectedRange();c.highlight=new l(c.highlight.row,c.highlight.col),c.to=a;var d=!1;do{d=!1;for(var e=0,f=this.mergeCells.mergedCellInfoCollection.length;f>e;e++){var g=this.mergeCells.mergedCellInfoCollection[e],h=new l(g.row,g.col),i=new l(g.row+g.rowspan-1,g.col+g.colspan-1),j=new m(h,h,i);c.expandByRange(j)&&(a.row=c.to.row,a.col=c.to.col,d=!0)}}while(d)}},Y=function(a,b){if(b&&"area"==b){var c=this.getSettings().mergeCells;if(c)for(var d=this.getSelectedRange(),e=new m(d.from,d.from,d.from),f=new m(d.to,d.to,d.to),g=0,h=this.mergeCells.mergedCellInfoCollection.length;h>g;g++){var i=this.mergeCells.mergedCellInfoCollection[g],j=new l(i.row,i.col),k=new l(i.row+i.rowspan-1,i.col+i.colspan-1),n=new m(j,j,k);e.expandByRange(n)&&(a[0]=e.from.row,a[1]=e.from.col),f.expandByRange(n)&&(a[2]=f.from.row,a[3]=f.from.col)}}},Z=function(a,b,c){var d=this.getSettings().mergeCells;if(d){var e=this.mergeCells.mergedCellInfoCollection.getInfo(a,b);!e||e.row==a&&e.col==b||(c.copyable=!1)}},$=function(a){var b=this.getSettings().mergeCells;if(b)for(var c,d=this.countCols(),e=0;d>e;e++){if(c=this.mergeCells.mergedCellInfoCollection.getInfo(a.startRow,e),c&&c.rowa.endRow)return a.endRow=f,$.call(this,a)}}},_=function(a){var b=this.getSettings().mergeCells;if(b)for(var c,d=this.countRows(),e=0;d>e;e++){if(c=this.mergeCells.mergedCellInfoCollection.getInfo(e,a.startColumn),c&&c.cola.endColumn)return a.endColumn=f,_.call(this,a)}}},ab=function(a){if(a&&this.mergeCells){var b=this.mergeCells.mergedCellInfoCollection,c=this.getSelectedRange();for(var d in b)if(c.highlight.row==b[d].row&&c.highlight.col==b[d].col&&c.to.row==b[d].row+b[d].rowspan-1&&c.to.col==b[d].col+b[d].colspan-1)return!1}return a};b.hooks.add("beforeInit",S),b.hooks.add("afterInit",T),b.hooks.add("beforeKeyDown",U),b.hooks.add("modifyTransformStart",W("modifyTransformStart")),b.hooks.add("modifyTransformEnd",W("modifyTransformEnd")),b.hooks.add("beforeSetRangeEnd",X),b.hooks.add("beforeDrawBorders",Y),b.hooks.add("afterIsMultipleSelection",ab),b.hooks.add("afterRenderer",N),b.hooks.add("afterContextMenuDefaultOptions",V),b.hooks.add("afterGetCellMeta",Z),b.hooks.add("afterViewportRowCalculatorOverride",$),b.hooks.add("afterViewportColumnCalculatorOverride",_),b.MergeCells=i,function(){function a(){}var c,d=function(a){return"boolean"==typeof a&&a===!0?!0:"object"==typeof a&&a.length>0?!0:!1},e=function(){d(this.getSettings().customBorders)&&(this.customBorders||(c=this,this.customBorders=new a))},f=function(a){for(var b=0;b=0?c.view.wt.selections[e]=d:c.view.wt.selections.push(d)},h=function(a,b,c){var d=o(a,b);d=p(d,c),this.setCellMeta(a,b,"borders",d),g(d)},i=function(a){for(var b=a.range,c=b.from.row;c<=b.to.row;c++)for(var d=b.from.col;d<=b.to.col;d++){var e=o(c,d),f=0;c==b.from.row&&(f++,a.hasOwnProperty("top")&&(e.top=a.top)),c==b.to.row&&(f++,a.hasOwnProperty("bottom")&&(e.bottom=a.bottom)),d==b.from.col&&(f++,a.hasOwnProperty("left")&&(e.left=a.left)),d==b.to.col&&(f++,a.hasOwnProperty("right")&&(e.right=a.right)),f>0&&(this.setCellMeta(c,d,"borders",e),g(e))}},j=function(a,b){return"border_row"+a+"col"+b},k=function(){return{width:1,color:"#000"}},l=function(){return{hide:!0}},n=function(){return{width:1,color:"#000",cornerVisible:!1}},o=function(a,b){return{className:j(a,b),border:n(),row:a,col:b,top:l(),right:l(),bottom:l(),left:l()}},p=function(a,b){return b.hasOwnProperty("border")&&(a.border=b.border),b.hasOwnProperty("top")&&(a.top=b.top),b.hasOwnProperty("right")&&(a.right=b.right),b.hasOwnProperty("bottom")&&(a.bottom=b.bottom),b.hasOwnProperty("left")&&(a.left=b.left),a},q=function(a){for(var b=document.querySelectorAll("."+a),c=0;c"+String.fromCharCode(10003)+""+a},w=function(a){this.getSettings().customBorders&&(a.items.push(b.ContextMenu.SEPARATOR),a.items.push({key:"borders",name:"Borders",submenu:{items:{top:{name:function(){var a="Top",b=u(this,"top");return b&&(a=v(a)),a},callback:function(){var a=u(this,"top");t.call(this,this.getSelectedRange(),"top",a)},disabled:!1},right:{name:function(){var a="Right",b=u(this,"right");return b&&(a=v(a)),a},callback:function(){var a=u(this,"right");t.call(this,this.getSelectedRange(),"right",a)},disabled:!1},bottom:{name:function(){var a="Bottom",b=u(this,"bottom");return b&&(a=v(a)),a},callback:function(){var a=u(this,"bottom");t.call(this,this.getSelectedRange(),"bottom",a)},disabled:!1},left:{name:function(){var a="Left",b=u(this,"left");return b&&(a=v(a)),a},callback:function(){var a=u(this,"left");t.call(this,this.getSelectedRange(),"left",a)},disabled:!1},remove:{name:"Remove border(s)",callback:function(){t.call(this,this.getSelectedRange(),"noBorders")},disabled:function(){return!u(this)}}}}}))};b.hooks.add("beforeInit",e),b.hooks.add("afterContextMenuDefaultOptions",w),b.hooks.add("afterInit",function(){var a=this.getSettings().customBorders;if(a){for(var b=0;b=0){l=b;var c=m.getBoundingClientRect();k=c.top,n.style.top=k+"px",n.style.left=c.left+"px",Q.rootElement.appendChild(n)}}function d(a,b){var c=a.getBoundingClientRect(),d=6;n.style.top=b>0?c.top+c.height-d+"px":c.top+"px"}function e(){var a=this;b.Dom.addClass(n,"active"),b.Dom.addClass(o,"active");var c=m.getBoundingClientRect();o.style.width=a.view.maximumVisibleElementWidth(0)+"px",o.style.height=c.height+"px",o.style.top=k+"px",o.style.left=n.style.left,a.rootElement.appendChild(o)}function f(a){o.style.top=k+a+"px"}function g(){b.Dom.removeClass(n,"active"),b.Dom.removeClass(o,"active")}var h,i,j,k,l,m,n=document.createElement("DIV"),o=document.createElement("DIV"),p=b.eventManager(this);n.className="manualRowMover",o.className="manualRowMoverGuide";var q=function(){var a=this;b.hooks.run(a,"persistentStateSave","manualRowPositions",a.manualRowPositions)},r=function(){var a=this,c={};return b.hooks.run(a,"persistentStateLoad","manualRowPositions",c),c.value},s=function(a){return"BODY"!=a.tagName?"TBODY"==a.parentNode.tagName?!0:(a=a.parentNode,s(a)):!1},t=function(a){return"TABLE"!=a.tagName?"TH"==a.tagName?a:t(a.parentNode):null},u=function(){var k,n=this;p.addEventListener(n.rootElement,"mouseover",function(a){if(s(a.target)){var b=t(a.target); +b&&(k?(i=n.view.wt.wtTable.getCoords(b).row,d(b,i-h)):c.call(n,b))}}),p.addEventListener(n.rootElement,"mousedown",function(a){b.Dom.hasClass(a.target,"manualRowMover")&&(j=b.helper.pageY(a),e.call(n),k=n,h=l,i=l)}),p.addEventListener(a,"mousemove",function(a){k&&f(b.helper.pageY(a)-j)}),p.addEventListener(a,"mouseup",function(){k&&(g(),k=!1,w(n.manualRowPositions,n.countRows()),n.manualRowPositions.splice(i,0,n.manualRowPositions.splice(h,1)[0]),n.forceFullRender=!0,n.view.render(),q.call(n),b.hooks.run(n,"afterRowMove",h,i),c.call(n,m))}),n.addHook("afterDestroy",v)},v=function(){p.clear()},w=function(a,b){if(a.lengthc;c++)a[c]=c};this.beforeInit=function(){this.manualRowPositions=[]},this.init=function(a){var b=this,c=!!b.getSettings().manualRowMove;if(c){var d=b.getSettings().manualRowMove,e=r.call(b);"undefined"!=typeof b.manualRowPositionsPluginUsages?b.manualRowPositionsPluginUsages.push("manualColumnMove"):b.manualRowPositionsPluginUsages=["manualColumnMove"],this.manualRowPositions="undefined"!=typeof e?e:Array.isArray(d)?d:[],"afterInit"===a&&(u.call(this),this.manualRowPositions.length>0&&(b.forceFullRender=!0,b.render()))}else{var f=b.manualRowPositionsPluginUsages?b.manualRowPositionsPluginUsages.indexOf("manualColumnMove"):-1;f>-1&&(v.call(this),b.manualRowPositions=[],b.manualRowPositionsPluginUsages[f]=void 0)}},this.modifyRow=function(a){var b=this;return b.getSettings().manualRowMove?("undefined"==typeof b.manualRowPositions[a]&&w(this.manualRowPositions,a+1),b.manualRowPositions[a]):a}}var d=new c;b.hooks.add("beforeInit",d.beforeInit),b.hooks.add("afterInit",function(){d.init.call(this,"afterInit")}),b.hooks.add("afterUpdateSettings",function(){d.init.call(this,"afterUpdateSettings")}),b.hooks.add("modifyRow",d.modifyRow),b.hooks.register("afterRowMove")}(b),function(b){function c(c){this.instance=c,this.addingStarted=!1;var d,e,f=!1,g=this,h=b.eventManager(c),i=function(){return c.autofill?void(c.autofill.handle&&c.autofill.handle.isDragged&&(c.autofill.handle.isDragged>1&&c.autofill.apply(),c.autofill.handle.isDragged=0,f=!1)):!0};h.addEventListener(document,"mouseup",function(a){i(a)}),h.addEventListener(document,"mousemove",function(c){if(!g.instance.autofill)return 0;var d=b.Dom.offset(g.instance.table).top-(a.pageYOffset||document.documentElement.scrollTop)+b.Dom.outerHeight(g.instance.table),e=b.Dom.offset(g.instance.table).left-(a.pageXOffset||document.documentElement.scrollLeft)+b.Dom.outerWidth(g.instance.table);g.addingStarted===!1&&g.instance.autofill.handle.isDragged>0&&c.clientY>d&&c.clientX<=e?(this.mouseDragOutside=!0,g.addingStarted=!0):this.mouseDragOutside=!1,this.mouseDragOutside&&setTimeout(function(){g.addingStarted=!1,g.instance.alter("insert_row")},200)}),d=this.instance.view.wt.wtSettings.settings.onCellCornerMouseDown,this.instance.view.wt.wtSettings.settings.onCellCornerMouseDown=function(a){c.autofill.handle.isDragged=1,f=!0,d(a)},e=this.instance.view.wt.wtSettings.settings.onCellMouseOver,this.instance.view.wt.wtSettings.settings.onCellMouseOver=function(a,b,d,g){c.autofill&&f&&!c.view.isMouseDown()&&c.autofill.handle&&c.autofill.handle.isDragged&&(c.autofill.handle.isDragged++,c.autofill.showBorder(b),c.autofill.checkIfNewRowNeeded()),e(a,b,d,g)},this.instance.view.wt.wtSettings.settings.onCellCornerDblClick=function(){c.autofill.selectAdjacent()}}c.prototype.init=function(){this.handle={}},c.prototype.disable=function(){this.handle.disabled=!0},c.prototype.selectAdjacent=function(){var a,b,c,d,e;a=this.instance.selection.isMultiple()?this.instance.view.wt.selections.area.getCorners():this.instance.view.wt.selections.current.getCorners(),b=this.instance.getData();a:for(c=a[2]+1;c=n;n++)e=parseInt(c[0][n],10),f=parseInt(c[h-1][n],10),g=("down"===d?f-e:e-f)/(h-1)||0,m.push(g);j.push(m)}if(-1!==["right","left"].indexOf(d))for(var o=0;k>=o;o++)e=parseInt(c[o][0],10),f=parseInt(c[o][i-1],10),g=("right"===d?f-e:e-f)/(i-1)||0,m=[],m.push(g),j.push(m);return j};this.instance.view.wt.selections.fill.clear(),c=this.instance.selection.isMultiple()?this.instance.view.wt.selections.area.getCorners():this.instance.view.wt.selections.current.getCorners();var h;if(a[0]===c[0]&&a[1]c[3]?(h="right",d=new l(a[0],c[3]+1),e=new l(a[2],a[3])):a[0]c[2]&&a[1]===c[1]&&(h="down",d=new l(c[2]+1,a[1]),e=new l(a[2],a[3])),d&&d.row>-1&&d.col>-1){var i={from:this.instance.getSelectedRange().from,to:this.instance.getSelectedRange().to};f=this.instance.getData(i.from.row,i.from.col,i.to.row,i.to.col);var j=g(d,e,f,h);b.hooks.run(this.instance,"beforeAutofill",d,e,f),this.instance.populateFromArray(d.row,d.col,f,e.row,e.col,"autofill",null,h,j),this.instance.selection.setRangeStart(new l(a[0],a[1])),this.instance.selection.setRangeEnd(new l(a[2],a[3]))}else this.instance.selection.refreshBorders()}},c.prototype.showBorder=function(a){var b=this.instance.getSelectedRange().getTopLeftCorner(),c=this.instance.getSelectedRange().getBottomRightCorner();if("horizontal"!==this.instance.getSettings().fillHandle&&(c.rowa.row))a=new l(a.row,c.col);else{if("vertical"===this.instance.getSettings().fillHandle)return;a=new l(c.row,a.col)}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(a),this.instance.view.render()},c.prototype.checkIfNewRowNeeded=function(){var a,b,c=this.instance.countRows(),d=this;this.instance.view.wt.selections.fill.cellRange&&this.addingStarted===!1&&(b=this.instance.getSelected(),a=this.instance.view.wt.selections.fill.getCorners(),b[2]=a;)c.push(a++);return c},k=function(a,b,c){var d=[],e={row:null,col:null};if("cols"==a)for(;c>=b;)e={row:-1,col:b++},d.push(e);else for(;c>=b;)e={row:b++,col:-1},d.push(e);for(var f=m(d),g=0,h=0,i=0;ib;b++)if(c[b].id==a)return c[b];return!1},p=function(a,b){for(var d=0,e=c.length;e>d;d++)if(c[d].level==b&&c[d].rows&&c[d].rows.indexOf(a)>-1)return c[d];return!1},q=function(a,b){for(var d=0,e=c.length;e>d;d++)if(c[d].level==b&&c[d].cols&&c[d].cols.indexOf(a)>-1)return c[d];return!1},r=function(){for(var a=[],b=0,d=c.length;d>b;b++)Array.isArray(c[b].cols)&&a.push(c[b]);return a},s=function(a){for(var b=[],d=0,e=c.length;e>d;d++)c[d].cols&&c[d].level===a&&b.push(c[d]);return b},t=function(){for(var a=[],b=0,d=c.length;d>b;b++)Array.isArray(c[b].rows)&&a.push(c[b]);return a},u=function(a){for(var b=[],d=0,e=c.length;e>d;d++)c[d].rows&&c[d].level===a&&b.push(c[d]);return b},v=function(a){var b=0;return a.length&&a.forEach(function(a){if(a=a.filter(function(a){return a.cols}),a.length){var c=a.sort(i("level","desc")),d=c[0].level;d>b&&(b=d)}}),b},w=function(a){var b=0;return a.length&&a.forEach(function(a){if(a=a.filter(function(a){return a.rows}),a.length){var c=a.sort(i("level","desc")),d=c[0].level;d>b&&(b=d)}}),b},x=function(a,b){var f=k("cols",a,b),g=v(f.groups);g===e.cols?e.cols++:g>e.cols&&(e.cols=g+1),d.cols||(d.cols=r().length),d.cols++,c.push({id:"c"+d.cols,level:g+1,cols:j(a,b),hidden:0})},y=function(a,b){var f=k("rows",a,b),g=w(f.groups);e.rows=Math.max(e.rows,g+1),d.rows||(d.rows=t().length),d.rows++,c.push({id:"r"+d.rows,level:g+1,rows:j(a,b),hidden:0})},z=function(a,b){for(var c,d=0,e=b.length;e>d;d++)if(b[d].hidden=a,c=b[d].level,f[c]||(f[c]=[]),g[c]||(g[c]=[]),b[d].rows)for(var h=0,i=b[d].rows.length;i>h;h++)f[c][b[d].rows[h]]=a>0?!0:void 0;else if(b[d].cols)for(var h=0,j=b[d].cols.length;j>h;h++)g[c][b[d].cols[h]]=a>0?!0:void 0},A=function(a,c,d,e){var f,g;switch(a){case"rows":f=p(c+1,d).id,g=b.Grouping.getGroupLevelsByRows();break;case"cols":f=q(c+1,d).id,g=b.Grouping.getGroupLevelsByCols()}return!!(g[c+1]&&g[c+1].indexOf(d)>-1&&e==f)},B=function(a,c,d,e){var f,g;switch(a){case"rows":f=p(c-1,d).id,g=b.Grouping.getGroupLevelsByRows();break;case"cols":f=q(c-1,d).id,g=b.Grouping.getGroupLevelsByCols()}return!!(g[c-1]&&g[c-1].indexOf(d)>-1&&e==f)},C=function(c,d,h,i){if(0===d)return!1;var j,k,l=B(c,d,h,i),m=A(c,d,h,i),n=!1;switch(c){case"rows":j=b.Grouping.getGroupLevelsByRows(),k=a.countRows();for(var o=0;o<=e.rows;o++)if(f[o]&&f[o][d+1]){n=!0;break}break;case"cols":j=b.Grouping.getGroupLevelsByCols(),k=a.countCols();for(var o=0;o<=e.cols;o++)if(g[o]&&g[o][d+1]){n=!0;break}}if(l){if(d==k-1)return!0;if(!m||m&&n)return!0;if(!j[d+1])return!0}return!1},D=function(b){var c;switch(b){case"rows":c=e.rows;for(var d=0;c>=d;d++)if(f[d]&&f[d][a.countRows()-1])return!0;break;case"cols":c=e.cols;for(var d=0;c>=d;d++)if(g[d]&&g[d][a.countCols()-1])return!0}return!1},E=function(c,d,h,i){var j,k,l,m=o(i),n=!1,p=function(a){for(var b=!1,c="rows"==a?f:g,h=0;h<=e[a];h++){for(l=d;m[a].indexOf(l)>-1;)b=!(!c[h]||!c[h][l]),l--;if(b)break}return b},q=B(c,d,h,i),r=A(c,d,h,i);switch(c){case"rows":j=b.Grouping.getGroupLevelsByRows(),k=a.countRows(),n=p(c);break;case"cols":j=b.Grouping.getGroupLevelsByCols(),k=a.countCols(),n=p(c)}if(d==k-1)return!1;if(0===d){if(r)return!0}else if(!q||q&&n){if(r)return!0}else if(!j[d-1]&&r)return!0;return!1},F=function(a,c,d,e,f){var g;switch(a){case"rows":g=p(c-1,d).id;break;case"cols":g=q(c-1,d).id}if(!g)return null;if(c>0&&B(a,c-1,d,g)&&g!=e){var i=document.createElement("DIV");return b.Dom.addClass(i,h.expandButton),i.id="htExpand-"+g,i.appendChild(document.createTextNode("+")),i.setAttribute("data-level",d),i.setAttribute("data-type",a),i.setAttribute("data-hidden","1"),f.appendChild(i),i}return null},G=function(a){for(var b=t(),c=r(),d=0,e=b.length;e>d;d++)if(b[d].rows.indexOf(a.row)>-1&&b[d].hidden)return!0;if(null===a.col)return!1;for(var d=0,f=c.length;f>d;d++)if(c[d].cols.indexOf(a.col)>-1&&c[d].hidden)return!0;return!1};return{getGroups:function(){return c},getLevels:function(){return e},instance:a,baseSpareRows:a.getSettings().minSpareRows,baseSpareCols:a.getSettings().minSpareCols,getRowGroups:t,getColGroups:r,init:function(){var c=a.getSettings().groups;c&&Array.isArray(c)&&b.Grouping.initGroups(c)},initGroups:function(a){c=[],a.forEach(function(a){var b=[],c=!1,d=!1;Array.isArray(a.rows)?(b=a.rows,c=!0):Array.isArray(a.cols)&&(b=a.cols,d=!0);var e=b[0],f=b[b.length-1];c?y(e,f):d&&x(e,f)})},resetGroups:function(){c=[],d={rows:0,cols:0},e={rows:0,cols:0};var a;for(var f in h)if("function"!=typeof h[f]){a=document.querySelectorAll("."+h[f]);for(var g=0,i=a.length;i>g;g++)b.Dom.removeClass(a[g],h[f])}for(var j=["htGroupColClosest","htGroupCol"],f=0,k=j.length;k>f;f++){a=document.querySelectorAll("."+j[f]);for(var g=0,i=a.length;i>g;g++)b.Dom.removeClass(a[g],j[f])}},updateGroups:function(){var a=this.getSettings().groups;b.Grouping.resetGroups(),b.Grouping.initGroups(a)},afterGetRowHeader:function(a,c){for(var d=!1,e=0,g=f.length;g>e;e++)f[e]&&f[e][a]===!0&&(d=!0);d?b.Dom.addClass(c.parentNode,"hidden"):!d&&b.Dom.hasClass(c.parentNode,"hidden")&&b.Dom.removeClass(c.parentNode,"hidden")},afterGetColHeader:function(c){var d=this.view.wt.wtSettings.getSetting("rowHeaders").length,e=a.rootElement.querySelectorAll("colgroup col:nth-child("+parseInt(c+d+1,10)+")");if(0!==e.length){for(var f=!1,h=0,i=g.length;i>h;h++)g[h]&&g[h][c]===!0&&(f=!0);if(f)for(var h=0,j=e.length;j>h;h++)b.Dom.addClass(e[h],"hidden");else if(!f&&b.Dom.hasClass(e[0],"hidden"))for(var h=0,j=e.length;j>h;h++)b.Dom.removeClass(e[h],"hidden")}},groupIndicatorsFactory:function(a,c){var f,g,i,j,k,l,m,n;switch(c){case"horizontal":f=b.Grouping.getGroupLevelsByCols(),g=function(a){return Array.prototype.indexOf.call(a.parentNode.parentNode.childNodes,a.parentNode)+1},i=function(a,b){return q(a,b).id},j="cols",k=function(a,b){return q(a-1,b)},l="columnHeaders",m=function(a){if(1===a.length){var c=a[0];a[0]=function(a,d,e){-1>a?r()(a,d,e):(b.Dom.removeClass(d,h.groupIndicatorContainer),c(a,d,e))}}return function(){return a}},n=!0;break;case"vertical":f=b.Grouping.getGroupLevelsByRows(),g=function(a){return b.Dom.index(a)+1},i=function(a,b){return p(a,b).id},j="rows",k=function(a,b){return p(a-1,b)},l="rowHeaders",m=function(a){return a}}var o=function(a){var c=document.createElement("div");return a.appendChild(c),{button:c,addClass:function(a){b.Dom.addClass(c,a)}}},r=function(){var a=c.charAt(0).toUpperCase()+c.slice(1);return function(c,d,e){e++;for(var g,l;g=d.lastChild;)d.removeChild(g);b.Dom.addClass(d,h.groupIndicatorContainer);var m=i(c,e);if(c>-1&&f[c]&&f[c].indexOf(e)>-1&&(l=o(d),l.addClass(h.groupIndicator(a)),E(j,c,e,m)&&l.addClass(h.groupStart),C(j,c,e,m)&&(l.button.appendChild(document.createTextNode("-")),l.addClass(h.collapseButton),l.button.id=h.collapseGroupId(m),l.button.setAttribute("data-level",e),l.button.setAttribute("data-type",j))),n){var p=b.Dom.index(d.parentNode);if(-1===c||-1>c&&p===b.Grouping.getLevels().cols+1||0===p&&0===b.Grouping.getLevels().cols)if(l=o(d),l.addClass(h.levelTrigger),-1===c)l.button.id=h.collapseFromLevel("Cols",e),l.button.appendChild(document.createTextNode(e));else if(-1>c&&p===b.Grouping.getLevels().cols+1||0===p&&0===b.Grouping.getLevels().cols){var q=b.Dom.index(d)+1;l.button.id=h.collapseFromLevel("Rows",q),l.button.appendChild(document.createTextNode(q))}}var r=F(j,c,e,m,d);if(c>0){var s=k(c-1,e);r&&s.hidden&&b.Dom.addClass(r,h.clickable)}cb()}};if(a=m(a),d[j]>0)for(var s=0;sc;c++)if(a[c].rows)for(var e=0,f=a[c].rows.length;f>e;e++)b[a[c].rows[e]]||(b[a[c].rows[e]]=[]),b[a[c].rows[e]].push(a[c].level);return b},getGroupLevelsByCols:function(){for(var a=r(),b=[],c=0,d=a.length;d>c;c++)if(a[c].cols)for(var e=0,f=a[c].cols.length;f>e;e++)b[a[c].cols[e]]||(b[a[c].cols[e]]=[]),b[a[c].cols[e]].push(a[c].level);return b},toggleGroupVisibility:function(c){if(b.Dom.hasClass(c.target,h.expandButton)||b.Dom.hasClass(c.target,h.collapseButton)||b.Dom.hasClass(c.target,h.levelTrigger)){var d,f,g,i,j=c.target,k=j.id.split("-"),l=[],m=function(a){a&&(j=a),k=j.id.split("-"),d=k[1],f=parseInt(j.getAttribute("data-level"),10),g=j.getAttribute("data-type"),i=parseInt(j.getAttribute("data-hidden")),i=isNaN(i)?1:i?0:1,j.setAttribute("data-hidden",i.toString()),l.push(o(d))};if(j.className.indexOf(h.levelTrigger)>-1){for(var n,p=[],q=[],r=j.id.indexOf("Rows")>-1?"rows":"cols",t=1,v=e[r];v>=t;t++)if(n="rows"==r?u(t):s(t),t>=parseInt(k[1],10))for(var w=0,x=n.length;x>w;w++)q.push(n[w]);else for(var w=0,x=n.length;x>w;w++)p.push(n[w]);z(!0,q),z(!1,p)}else m(),z(i,l);g=g||r;var y=D(g),A=g.charAt(0).toUpperCase()+g.slice(1),B=b.Grouping["baseSpare"+A];y?0==B&&(a.alter("insert_"+g.slice(0,-1),a["count"+A]()),b.Grouping["dummy"+g.slice(0,-1)]=!0):0==B&&b.Grouping["dummy"+g.slice(0,-1)]&&(a.alter("remove_"+g.slice(0,-1),a["count"+A]()-1),b.Grouping["dummy"+g.slice(0,-1)]=!1),a.render(),c.stopImmediatePropagation()}},modifySelectionFactory:function(a){var b,c=this.instance,d=new l(0,0),e=function(a,b){var c=0;switch(a){case"down":for(;G(b);)c++,b.row+=1;break;case"up":for(;G(b);)c--,b.row-=1;break;case"right":for(;G(b);)c++,b.col+=1;break;case"left":for(;G(b);)c--,b.col-=1}return c},f=function(a,b){a.row>0?G(b)&&(a.row+=e("down",b)):a.row<0&&G(b)&&(a.row+=e("up",b)),a.col>0?G(b)&&(a.col+=e("right",b)):a.col<0&&G(b)&&(a.col+=e("left",b))};switch(a){case"start":return function(a){b=c.getSelected(),d.row=b[0]+a.row,d.col=b[1]+a.col,f(a,d)};case"end":return function(a){b=c.getSelected(),d.row=b[2]+a.row,d.col=b[3]+a.col,f(a,d)}}},modifyRowHeight:function(b,c){return a.view.wt.wtTable.rowFilter&&G({row:c,col:null})?0:void 0},validateGroups:function(){for(var b=function(a,b){return a[0]b[0]&&b[1]f;f++)if(c[f].rows){if(1===c[f].rows.length)throw new Error("Grouping error: Group {"+c[f].rows[0]+"} is invalid. Cannot define single-entry groups.");if(0===c[f].rows.length)throw new Error("Grouping error: Cannot define empty groups.");e.push(c[f].rows);for(var h=0,i=e.length;i>h;h++)if(b(c[f].rows,e[h]))throw new Error("Grouping error: ranges {"+c[f].rows[0]+", "+c[f].rows[1]+"} and {"+e[h][0]+", "+e[h][1]+"} are overlapping.")}else if(c[f].cols){if(1===c[f].cols.length)throw new Error("Grouping error: Group {"+c[f].cols[0]+"} is invalid. Cannot define single-entry groups.");if(0===c[f].cols.length)throw new Error("Grouping error: Cannot define empty groups.");d.push(c[f].cols);for(var h=0,j=d.length;j>h;h++)if(b(c[f].cols,d[h]))throw new Error("Grouping error: ranges {"+c[f].cols[0]+", "+c[f].cols[1]+"} and {"+d[h][0]+", "+d[h][1]+"} are overlapping.")}return!0},afterGetRowHeaderRenderers:function(a){b.Grouping.groupIndicatorsFactory(a,"vertical")},afterGetColumnHeaderRenderers:function(a){b.Grouping.groupIndicatorsFactory(a,"horizontal")},hookProxy:function(c,d){return function(){return a.getSettings().groups?d?b.Grouping[c](d).apply(this,arguments):b.Grouping[c].apply(this,arguments):void 0}}}},M=function(){var a=this,c=!!a.getSettings().groups;if(c){var d={};b.Grouping=new bb(a),a.getSettings().rowHeaders||(d.rowHeaders=!0),a.getSettings().colHeaders||(d.colHeaders=!0),(d.colHeaders||d.rowHeaders)&&a.updateSettings(d);var e=b.Grouping.validateGroups();if(!e)return;a.addHook("beforeInit",b.Grouping.hookProxy("init")),a.addHook("afterUpdateSettings",b.Grouping.hookProxy("updateGroups")),a.addHook("afterGetColumnHeaderRenderers",b.Grouping.hookProxy("afterGetColumnHeaderRenderers")),a.addHook("afterGetRowHeaderRenderers",b.Grouping.hookProxy("afterGetRowHeaderRenderers")),a.addHook("afterGetRowHeader",b.Grouping.hookProxy("afterGetRowHeader")),a.addHook("afterGetColHeader",b.Grouping.hookProxy("afterGetColHeader")),a.addHook("beforeOnCellMouseDown",b.Grouping.hookProxy("toggleGroupVisibility")),a.addHook("modifyTransformStart",b.Grouping.hookProxy("modifySelectionFactory","start")),a.addHook("modifyTransformEnd",b.Grouping.hookProxy("modifySelectionFactory","end")),a.addHook("modifyRowHeight",b.Grouping.hookProxy("modifyRowHeight"))}},cb=function(){for(var a=document.querySelectorAll("colgroup"),c=0,d=a.length;d>c;c++){var e=a[c].querySelectorAll("col.rowHeader");if(0===e.length)return;for(var f=0,g=e.length+1;g>f;f++){if(2==g)return;ff;f++)if("copy"===e[f].key){this.zeroClipboardInstance=new ZeroClipboard(b.getCell(f,0)),this.zeroClipboardInstance.off(),this.zeroClipboardInstance.on("copy",function(a){var b=a.clipboardData;b.setData("text/plain",d.copy()),d.instance.getSettings().outsideClickDeselects=d.outsideClickDeselectsCache}),c.bindEvents();break}},b.prototype.bindEvents=function(){var b=this;if(b.cmInstance){var c=new a.eventManager(this.instance),d=function(){var c=b.cmInstance.rootElement.querySelector("td.current");c&&a.Dom.removeClass(c,"current"),b.outsideClickDeselectsCache=b.instance.getSettings().outsideClickDeselects,b.instance.getSettings().outsideClickDeselects=!1},e=function(){var c=b.cmInstance.rootElement.querySelector("td.zeroclipboard-is-hover");c&&a.Dom.removeClass(c,"zeroclipboard-is-hover"),b.instance.getSettings().outsideClickDeselects=b.outsideClickDeselectsCache};c.removeEventListener(document,"mouseenter",function(){d()}),c.addEventListener(document,"mouseenter",function(){d()}),c.removeEventListener(document,"mouseleave",function(){e()}),c.addEventListener(document,"mouseleave",function(){e()})}},b.prototype.init=function(){if(this.getSettings().contextMenuCopyPaste){if("object"==typeof this.getSettings().contextMenuCopyPaste&&(c.swfPath=this.getSettings().contextMenuCopyPaste.swfPath),"undefined"==typeof ZeroClipboard)throw new Error("To be able to use the Copy/Paste feature from the context menu, you need to manualy include ZeroClipboard.js file to your website.");try{{new ActiveXObject("ShockwaveFlash.ShockwaveFlash")}}catch(a){if("undefined"==typeof navigator.mimeTypes["application/x-shockwave-flash"])throw new Error("To be able to use the Copy/Paste feature from the context menu, your browser needs to have Flash Plugin installed.")}c.instance=this,c.prepareZeroClipboard()}};var c=new b;a.hooks.add("afterRender",function(){c.setupZeroClipboard(this)}),a.hooks.add("afterInit",c.init),a.hooks.add("afterContextMenuDefaultOptions",c.addToContextMenu),a.ContextMenuCopyPaste=b}(b),function(a){function b(b){this.instance=b,this.dragged=[],this.eventManager=a.eventManager(b),this.bindTouchEvents()}b.prototype.getCurrentRangeCoords=function(a,b,c,d,e){var f=a.getTopLeftCorner(),g=a.getBottomRightCorner(),h=a.getBottomLeftCorner(),i=a.getTopRightCorner(),j={start:null,end:null};switch(c){case"NE-SW":switch(d){case"NE-SW":case"NW-SE":j="topLeft"==e?{start:new l(b.row,a.highlight.col),end:new l(h.row,b.col)}:{start:new l(a.highlight.row,b.col),end:new l(b.row,f.col)};break;case"SE-NW":"bottomRight"==e&&(j={start:new l(g.row,b.col),end:new l(b.row,f.col)})}break;case"NW-SE":switch(d){case"NE-SW":"topLeft"==e?j={start:b,end:h}:j.end=b;break;case"NW-SE":"topLeft"==e?j={start:b,end:g}:j.end=b;break;case"SE-NW":"topLeft"==e?j={start:b,end:f}:j.end=b;break;case"SW-NE":"topLeft"==e?j={start:b,end:i}:j.end=b}break;case"SW-NE":switch(d){case"NW-SE":j="bottomRight"==e?{start:new l(b.row,f.col),end:new l(h.row,b.col)}:{start:new l(f.row,b.col),end:new l(b.row,g.col)};break;case"SW-NE":j="topLeft"==e?{start:new l(a.highlight.row,b.col),end:new l(b.row,g.col)}:{start:new l(b.row,f.col),end:new l(f.row,b.col)};break;case"SE-NW":"bottomRight"==e?j={start:new l(b.row,i.col),end:new l(f.row,b.col)}:"topLeft"==e&&(j={start:h,end:b})}break;case"SE-NW":switch(d){case"NW-SE":case"NE-SW":case"SW-NE":"topLeft"==e&&(j.end=b);break;case"SE-NW":"topLeft"==e?j.end=b:j={start:b,end:f}}}return j},b.prototype.bindTouchEvents=function(){var b=this,c=function(a){if(1==this.dragged.length)return this.dragged=[],!0;var b=this.dragged.indexOf(a);return-1==b?!1:void(0===b?this.dragged=this.dragged.slice(0,1):1==b&&(this.dragged=this.dragged.slice(-1)))};this.eventManager.addEventListener(this.instance.rootElement,"touchstart",function(c){if(a.Dom.hasClass(c.target,"topLeftSelectionHandle-HitArea")){b.dragged.push("topLeft");var d=b.instance.getSelectedRange();return b.touchStartRange={width:d.getWidth(),height:d.getHeight(),direction:d.getDirection()},c.preventDefault(),!1}if(a.Dom.hasClass(c.target,"bottomRightSelectionHandle-HitArea")){b.dragged.push("bottomRight");var d=b.instance.getSelectedRange();return b.touchStartRange={width:d.getWidth(),height:d.getHeight(),direction:d.getDirection()},c.preventDefault(),!1}}),this.eventManager.addEventListener(this.instance.rootElement,"touchend",function(d){return a.Dom.hasClass(d.target,"topLeftSelectionHandle-HitArea")?(c.call(b,"topLeft"),b.touchStartRange=void 0,d.preventDefault(),!1):a.Dom.hasClass(d.target,"bottomRightSelectionHandle-HitArea")?(c.call(b,"bottomRight"),b.touchStartRange=void 0,d.preventDefault(),!1):void 0}),this.eventManager.addEventListener(this.instance.rootElement,"touchmove",function(c){var d=a.Dom.getWindowScrollTop(),e=a.Dom.getWindowScrollLeft();if(b.dragged.length>0){var f=document.elementFromPoint(c.touches[0].screenX-e,c.touches[0].screenY-d);if(!f)return;if("TD"==f.nodeName||"TH"==f.nodeName){var g=b.instance.getCoords(f);-1==g.col&&(g.col=0);var h=b.instance.getSelectedRange(),i=h.getWidth(),j=h.getHeight(),k=h.getDirection();1==i&&1==j&&b.instance.selection.setRangeEnd(g);var l=b.getCurrentRangeCoords(h,g,b.touchStartRange.direction,k,b.dragged[0]);null!=l.start&&b.instance.selection.setRangeStart(l.start),b.instance.selection.setRangeEnd(l.end)}c.preventDefault()}})},b.prototype.isDragged=function(){return 0===this.dragged.length?!1:!0};var c=function(){var c=this;a.plugins.multipleSelectionHandles=new b(c)};a.hooks.add("afterInit",c)}(b);var db=function(){function a(){}return a.prototype.init=function(a){this.instance=a,this.bindEvents(),this.scrollbars=[this.instance.view.wt.wtScrollbars.vertical,this.instance.view.wt.wtScrollbars.horizontal,this.instance.view.wt.wtScrollbars.corner],this.clones=[this.instance.view.wt.wtScrollbars.vertical.clone.wtTable.holder.parentNode,this.instance.view.wt.wtScrollbars.horizontal.clone.wtTable.holder.parentNode,this.instance.view.wt.wtScrollbars.corner.clone.wtTable.holder.parentNode]},a.prototype.bindEvents=function(){var a=this;this.instance.addHook("beforeTouchScroll",function(){b.freezeOverlays=!0;for(var c=0,d=a.clones.length;d>c;c++)b.Dom.addClass(a.clones[c],"hide-tween")}),this.instance.addHook("afterMomentumScroll",function(){b.freezeOverlays=!1;for(var c=0,d=a.clones.length;d>c;c++)b.Dom.removeClass(a.clones[c],"hide-tween");for(var c=0,d=a.clones.length;d>c;c++)b.Dom.addClass(a.clones[c],"show-tween");setTimeout(function(){for(var c=0,d=a.clones.length;d>c;c++)b.Dom.removeClass(a.clones[c],"show-tween")},400);for(var c=0,d=a.scrollbars.length;d>c;c++)a.scrollbars[c].refresh(),a.scrollbars[c].resetFixedPosition()})},a}(),eb=new db;if(b.hooks.add("afterInit",function(){eb.init.call(eb,this)}),function(a){function b(b){function c(c){c.items.push(a.ContextMenu.SEPARATOR,{key:"freeze_column",name:function(){var a=b.getSelected()[1];return a>o-1?"Freeze this column":"Unfreeze this column"},disabled:function(){var a=b.getSelected();return a[1]!==a[3]},callback:function(){var a=b.getSelected()[1];a>o-1?j(a):k(a)}})}function d(){b.updateSettings({fixedColumnsLeft:o+1}),o++}function e(){b.updateSettings({fixedColumnsLeft:o-1}),o--}function f(a){b.manualColumnPositions&&0!==b.manualColumnPositions.length||b.manualColumnPositions||(b.manualColumnPositions=[]),a?b.manualColumnPositions[a]||g(a+1):g(b.countCols())}function g(a){if(b.manualColumnPositions.lengthc;c++)b.manualColumnPositions[c]=c}function h(a,c,d,e){null==d&&(d=a),"freeze"===e?b.manualColumnPositions.splice(o,0,b.manualColumnPositions.splice(c,1)[0]):"unfreeze"===e&&b.manualColumnPositions.splice(d,0,b.manualColumnPositions.splice(c,1)[0])}function i(a){for(var b=o,c=l(b),d=l(a);d>c;)b++,c=l(b);return b-1}function j(a){if(!(o-1>=a)){var b=l(a)||a;f(b),h(b,a,null,"freeze"),d()}}function k(a){if(!(a>o-1)){var b=i(a),c=l(a)||a;f(c),h(c,a,b,"unfreeze"),e()}}function l(a){return b.manualColumnPositions[a]}function m(a){return this.manualColumnPositionsPluginUsages.length>1?a:l(a)}function n(){b.addHook("modifyCol",m),b.addHook("afterContextMenuDefaultOptions",c)}var o=b.getSettings().fixedColumnsLeft,p=function(){"undefined"!=typeof b.manualColumnPositionsPluginUsages?b.manualColumnPositionsPluginUsages.push("manualColumnFreeze"):b.manualColumnPositionsPluginUsages=["manualColumnFreeze"],n()};return{init:p,freezeColumn:j,unfreezeColumn:k,helpers:{addFixedColumn:d,removeFixedColumn:e,checkPositionData:f,modifyColumnOrder:h,getBestColumnReturnPosition:i}}}var c=function(){if(this.getSettings().manualColumnFreeze){var c;a.plugins.manualColumnFreeze=b,this.manualColumnFreeze=new b(this),c=this.manualColumnFreeze,c.init.call(this)}};a.hooks.add("beforeInit",c)}(b),j.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)},j.prototype.makeClone=function(a){var b=document.createElement("DIV");b.className="ht_clone_"+a+" handsontable",b.style.position="absolute",b.style.top=0,b.style.left=0,b.style.overflow="hidden";var c=document.createElement("TABLE");return c.className=this.instance.wtTable.TABLE.className,b.appendChild(c),this.instance.wtTable.holder.parentNode.appendChild(b),new p({cloneSource:this.instance,cloneOverlay:this,table:c})},j.prototype.getScrollableElement=function(b){for(var c=b.parentNode;c&&c.style;){if("visible"!==c.style.overflow&&""!==c.style.overflow)return c;if(this instanceof w&&"visible"!==c.style.overflowX&&""!==c.style.overflowX)return c;c=c.parentNode}return a},j.prototype.refresh=function(a){this.clone&&this.clone.draw(a)},j.prototype.destroy=function(){var a=b.eventManager(this.clone);a.clear()},k.prototype.appear=function(a){var c,d,e,f,g,h,i,j,k,m,n,o;if(!this.disabled){var p,q,r,s,t,u,w,y=this.instance,z=function(){return!this.instance.selections.area.cellRange||r==this.instance.selections.area.cellRange.to.row&&s==this.instance.selections.area.cellRange.to.col?!1:!0},A=function(a,b,c,d){var e=parseInt(this.selectionHandles.styles.topLeft.width,10),f=parseInt(this.selectionHandles.styles.topLeftHitArea.width,10); +this.selectionHandles.styles.topLeft.top=parseInt(a-e,10)+"px",this.selectionHandles.styles.topLeft.left=parseInt(b-e,10)+"px",this.selectionHandles.styles.topLeftHitArea.top=parseInt(a-f/4*3,10)+"px",this.selectionHandles.styles.topLeftHitArea.left=parseInt(b-f/4*3,10)+"px",this.selectionHandles.styles.bottomRight.top=parseInt(a+d,10)+"px",this.selectionHandles.styles.bottomRight.left=parseInt(b+c,10)+"px",this.selectionHandles.styles.bottomRightHitArea.top=parseInt(a+d-f/4,10)+"px",this.selectionHandles.styles.bottomRightHitArea.left=parseInt(b+c-f/4,10)+"px",this.settings.border.multipleSelectionHandlesVisible&&this.settings.border.multipleSelectionHandlesVisible()?(this.selectionHandles.styles.topLeft.display="block",this.selectionHandles.styles.topLeftHitArea.display="block",z.call(this)?(this.selectionHandles.styles.bottomRight.display="none",this.selectionHandles.styles.bottomRightHitArea.display="none"):(this.selectionHandles.styles.bottomRight.display="block",this.selectionHandles.styles.bottomRightHitArea.display="block")):(this.selectionHandles.styles.topLeft.display="none",this.selectionHandles.styles.bottomRight.display="none",this.selectionHandles.styles.topLeftHitArea.display="none",this.selectionHandles.styles.bottomRightHitArea.display="none"),p==this.instance.wtSettings.getSetting("fixedRowsTop")||q==this.instance.wtSettings.getSetting("fixedColumnsLeft")?(this.selectionHandles.styles.topLeft.zIndex="9999",this.selectionHandles.styles.topLeftHitArea.zIndex="9999"):(this.selectionHandles.styles.topLeft.zIndex="",this.selectionHandles.styles.topLeftHitArea.zIndex="")};for(u=y.cloneOverlay instanceof x||y.cloneOverlay instanceof v?y.getSetting("fixedRowsTop"):y.wtTable.getRenderedRowsCount(),t=0;u>t;t++)if(w=y.wtTable.rowFilter.renderedToSource(t),w>=a[0]&&w<=a[2]){p=w;break}for(t=u-1;t>=0;t--)if(w=y.wtTable.rowFilter.renderedToSource(t),w>=a[0]&&w<=a[2]){r=w;break}for(u=y.wtTable.getRenderedColumnsCount(),t=0;u>t;t++)if(w=y.wtTable.columnFilter.renderedToSource(t),w>=a[1]&&w<=a[3]){q=w;break}for(t=u-1;t>=0;t--)if(w=y.wtTable.columnFilter.renderedToSource(t),w>=a[1]&&w<=a[3]){s=w;break}if(void 0===p||void 0===q)return void this.disappear();c=p!==r||q!==s,d=y.wtTable.getCell(new l(p,q)),e=c?y.wtTable.getCell(new l(r,s)):d,f=b.Dom.offset(d),g=c?b.Dom.offset(e):f,h=b.Dom.offset(y.wtTable.TABLE),j=f.top,n=g.top+b.Dom.outerHeight(e)-j,m=f.left,o=g.left+b.Dom.outerWidth(e)-m,i=j-h.top-1,k=m-h.left-1;var B=b.Dom.getComputedStyle(d);parseInt(B.borderTopWidth,10)>0&&(i+=1,n=n>0?n-1:0),parseInt(B.borderLeftWidth,10)>0&&(k+=1,o=o>0?o-1:0),this.topStyle.top=i+"px",this.topStyle.left=k+"px",this.topStyle.width=o+"px",this.topStyle.display="block",this.leftStyle.top=i+"px",this.leftStyle.left=k+"px",this.leftStyle.height=n+"px",this.leftStyle.display="block";var C=Math.floor(this.settings.border.width/2);if(this.bottomStyle.top=i+n-C+"px",this.bottomStyle.left=k+"px",this.bottomStyle.width=o+"px",this.bottomStyle.display="block",this.rightStyle.top=i+"px",this.rightStyle.left=k+o-C+"px",this.rightStyle.height=n+1+"px",this.rightStyle.display="block",b.mobileBrowser||!this.hasSetting(this.settings.border.cornerVisible)||z.call(this))this.cornerStyle.display="none";else if(this.cornerStyle.top=i+n-4+"px",this.cornerStyle.left=k+o-4+"px",this.cornerStyle.borderRightWidth=this.cornerDefaultStyle.borderWidth,this.cornerStyle.width=this.cornerDefaultStyle.width,this.cornerStyle.display="block",!y.cloneOverlay&&s===y.wtTable.getRenderedColumnsCount()-1){var D=b.Dom.getScrollableElement(y.wtTable.TABLE),E=e.offsetLeft+b.Dom.outerWidth(e)>=b.Dom.innerWidth(D);E&&(this.cornerStyle.borderRightWidth="0px",this.cornerStyle.width=Math.ceil(parseInt(this.cornerDefaultStyle.width,10)/2)+"px")}b.mobileBrowser&&A.call(this,i,k,o,n)}},k.prototype.disappear=function(){this.topStyle.display="none",this.leftStyle.display="none",this.bottomStyle.display="none",this.rightStyle.display="none",this.cornerStyle.display="none",b.mobileBrowser&&(this.selectionHandles.styles.topLeft.display="none",this.selectionHandles.styles.bottomRight.display="none")},k.prototype.hasSetting=function(a){return"function"==typeof a?a():!!a},l.prototype.isValid=function(a){return this.row<0||this.col<0?!1:this.row>=a.getSetting("totalRows")||this.col>=a.getSetting("totalColumns")?!1:!0},l.prototype.isEqual=function(a){return a===this?!0:this.row===a.row&&this.col===a.col},l.prototype.isSouthEastOf=function(a){return this.row>=a.row&&this.col>=a.col},l.prototype.isNorthWestOf=function(a){return this.row<=a.row&&this.col<=a.col},l.prototype.isSouthWestOf=function(a){return this.row>=a.row&&this.col<=a.col},l.prototype.isNorthEastOf=function(a){return this.row<=a.row&&this.col>=a.col},a.WalkontableCellCoords=l,m.prototype.isValid=function(a){return this.from.isValid(a)&&this.to.isValid(a)},m.prototype.isSingle=function(){return this.from.row===this.to.row&&this.from.col===this.to.col},m.prototype.getHeight=function(){return Math.max(this.from.row,this.to.row)-Math.min(this.from.row,this.to.row)+1},m.prototype.getWidth=function(){return Math.max(this.from.col,this.to.col)-Math.min(this.from.col,this.to.col)+1},m.prototype.includes=function(a){var b=this.getTopLeftCorner(),c=this.getBottomRightCorner();return a.row<0&&(a.row=0),a.col<0&&(a.col=0),b.row<=a.row&&c.row>=a.row&&b.col<=a.col&&c.col>=a.col},m.prototype.includesRange=function(a){return this.includes(a.getTopLeftCorner())&&this.includes(a.getBottomRightCorner())},m.prototype.isEqual=function(a){return Math.min(this.from.row,this.to.row)==Math.min(a.from.row,a.to.row)&&Math.max(this.from.row,this.to.row)==Math.max(a.from.row,a.to.row)&&Math.min(this.from.col,this.to.col)==Math.min(a.from.col,a.to.col)&&Math.max(this.from.col,this.to.col)==Math.max(a.from.col,a.to.col)},m.prototype.overlaps=function(a){return a.isSouthEastOf(this.getTopLeftCorner())&&a.isNorthWestOf(this.getBottomRightCorner())},m.prototype.isSouthEastOf=function(a){return this.getTopLeftCorner().isSouthEastOf(a)||this.getBottomRightCorner().isSouthEastOf(a)},m.prototype.isNorthWestOf=function(a){return this.getTopLeftCorner().isNorthWestOf(a)||this.getBottomRightCorner().isNorthWestOf(a)},m.prototype.expand=function(a){var b=this.getTopLeftCorner(),c=this.getBottomRightCorner();return a.rowc.row||a.col>c.col?(this.from=new l(Math.min(b.row,a.row),Math.min(b.col,a.col)),this.to=new l(Math.max(c.row,a.row),Math.max(c.col,a.col)),!0):!1},m.prototype.expandByRange=function(a){if(this.includesRange(a)||!this.overlaps(a))return!1;var b=this.getTopLeftCorner(),c=this.getBottomRightCorner(),d=(this.getTopRightCorner(),this.getBottomLeftCorner(),a.getTopLeftCorner()),e=a.getBottomRightCorner(),f=Math.min(b.row,d.row),g=Math.min(b.col,d.col),h=Math.max(c.row,e.row),i=Math.max(c.col,e.col),j=new l(f,g),k=new l(h,i),n=new m(j,j,k).isCorner(this.from,a),o=a.isEqual(new m(j,j,k));return n&&!o&&(this.from.col>j.col&&(j.col=i,k.col=g),this.from.row>j.row&&(j.row=h,k.row=f)),this.from=j,this.to=k,!0},m.prototype.getDirection=function(){return this.from.isNorthWestOf(this.to)?"NW-SE":this.from.isNorthEastOf(this.to)?"NE-SW":this.from.isSouthEastOf(this.to)?"SE-NW":this.from.isSouthWestOf(this.to)?"SW-NE":void 0},m.prototype.setDirection=function(a){switch(a){case"NW-SE":this.from=this.getTopLeftCorner(),this.to=this.getBottomRightCorner();break;case"NE-SW":this.from=this.getTopRightCorner(),this.to=this.getBottomLeftCorner();break;case"SE-NW":this.from=this.getBottomRightCorner(),this.to=this.getTopLeftCorner();break;case"SW-NE":this.from=this.getBottomLeftCorner(),this.to=this.getTopRightCorner()}},m.prototype.getTopLeftCorner=function(){return new l(Math.min(this.from.row,this.to.row),Math.min(this.from.col,this.to.col))},m.prototype.getBottomRightCorner=function(){return new l(Math.max(this.from.row,this.to.row),Math.max(this.from.col,this.to.col))},m.prototype.getTopRightCorner=function(){return new l(Math.min(this.from.row,this.to.row),Math.max(this.from.col,this.to.col))},m.prototype.getBottomLeftCorner=function(){return new l(Math.max(this.from.row,this.to.row),Math.min(this.from.col,this.to.col))},m.prototype.isCorner=function(a,b){return b&&b.includes(a)&&(this.getTopLeftCorner().isEqual(new l(b.from.row,b.from.col))||this.getTopRightCorner().isEqual(new l(b.from.row,b.to.col))||this.getBottomLeftCorner().isEqual(new l(b.to.row,b.from.col))||this.getBottomRightCorner().isEqual(new l(b.to.row,b.to.col)))?!0:a.isEqual(this.getTopLeftCorner())||a.isEqual(this.getTopRightCorner())||a.isEqual(this.getBottomLeftCorner())||a.isEqual(this.getBottomRightCorner())},m.prototype.getOppositeCorner=function(a,b){if(!(a instanceof l))return!1;if(b&&b.includes(a)){if(this.getTopLeftCorner().isEqual(new l(b.from.row,b.from.col)))return this.getBottomRightCorner();if(this.getTopRightCorner().isEqual(new l(b.from.row,b.to.col)))return this.getBottomLeftCorner();if(this.getBottomLeftCorner().isEqual(new l(b.to.row,b.from.col)))return this.getTopRightCorner();if(this.getBottomRightCorner().isEqual(new l(b.to.row,b.to.col)))return this.getTopLeftCorner()}return a.isEqual(this.getBottomRightCorner())?this.getTopLeftCorner():a.isEqual(this.getTopLeftCorner())?this.getBottomRightCorner():a.isEqual(this.getTopRightCorner())?this.getBottomLeftCorner():a.isEqual(this.getBottomLeftCorner())?this.getTopRightCorner():void 0},m.prototype.getBordersSharedWith=function(a){if(!this.includesRange(a))return[];var b={top:Math.min(this.from.row,this.to.row),bottom:Math.max(this.from.row,this.to.row),left:Math.min(this.from.col,this.to.col),right:Math.max(this.from.col,this.to.col)},c={top:Math.min(a.from.row,a.to.row),bottom:Math.max(a.from.row,a.to.row),left:Math.min(a.from.col,a.to.col),right:Math.max(a.from.col,a.to.col)},d=[];return b.top==c.top&&d.push("top"),b.right==c.right&&d.push("right"),b.bottom==c.bottom&&d.push("bottom"),b.left==c.left&&d.push("left"),d},m.prototype.getInner=function(){for(var a=this.getTopLeftCorner(),b=this.getBottomRightCorner(),c=[],d=a.row;d<=b.row;d++)for(var e=a.col;e<=b.col;e++)this.from.row===d&&this.from.col===e||this.to.row===d&&this.to.col===e||c.push(new l(d,e));return c},m.prototype.getAll=function(){for(var a=this.getTopLeftCorner(),b=this.getBottomRightCorner(),c=[],d=a.row;d<=b.row;d++)for(var e=a.col;e<=b.col;e++)c.push(a.row===d&&a.col===e?a:b.row===d&&b.col===e?b:new l(d,e));return c},m.prototype.forAll=function(a){for(var b=this.getTopLeftCorner(),c=this.getBottomRightCorner(),d=b.row;d<=c.row;d++)for(var e=b.col;e<=c.col;e++){var f=a(d,e);if(f===!1)return}},a.WalkontableCellRange=m,n.prototype.offsetted=function(a){return a+this.offset},n.prototype.unOffsetted=function(a){return a-this.offset},n.prototype.renderedToSource=function(a){return this.offsetted(a)},n.prototype.sourceToRendered=function(a){return this.unOffsetted(a)},n.prototype.offsettedTH=function(a){return a-this.countTH},n.prototype.unOffsettedTH=function(a){return a+this.countTH},n.prototype.visibleRowHeadedColumnToSourceColumn=function(a){return this.renderedToSource(this.offsettedTH(a))},n.prototype.sourceColumnToVisibleRowHeadedColumn=function(a){return this.unOffsettedTH(this.sourceToRendered(a))},o.prototype.getContainerSize=function(){return"function"==typeof this.containerSizeFn?this.containerSizeFn():this.containerSizeFn},o.prototype.getSize=function(a){return this.cellSizes[a]+(this.cellStretch[a]||0)},o.prototype.stretch=function(){var a=this.getContainerSize(),b=0;if(this.remainingSize=this.cellSizesSum-a,this.cellStretch.length=0,"all"===this.strategy){if(this.remainingSize<0){for(var c,d=a/this.cellSizesSum;b=g},p.prototype.draw=function(a){return this.drawInterrupted=!1,a||b.Dom.isVisible(this.wtTable.TABLE)?(this.wtTable.draw(a),this):void(this.drawInterrupted=!0)},p.prototype.getCell=function(a,b){if(b){var c=this.wtSettings.getSetting("fixedRowsTop"),d=this.wtSettings.getSetting("fixedColumnsLeft");return a.rowb-1)throw new Error("row "+a.row+" does not exist");if(a.col<0||a.col>c-1)throw new Error("column "+a.col+" does not exist");a.row>this.instance.wtTable.getLastVisibleRow()?this.instance.wtScrollbars.vertical.scrollTo(a.row,!0):a.row>=this.instance.getSetting("fixedRowsTop")&&a.rowthis.instance.wtTable.getLastVisibleColumn()?this.instance.wtScrollbars.horizontal.scrollTo(a.col,!0):a.col>this.instance.getSetting("fixedColumnsLeft")&&a.colh&&j-e.offsetWidth>0?-h+"px":"0",d=0>g&&i-e.offsetHeight>0?-g+"px":"0"}else b.freezeOverlays||(c=this.instance.wtScrollbars.horizontal.getScrollPosition()+"px",d=this.instance.wtScrollbars.vertical.getScrollPosition()+"px");b.Dom.setOverlayPosition(e,c,d),e.style.width=b.Dom.outerWidth(this.clone.wtTable.TABLE)+4+"px",e.style.height=b.Dom.outerHeight(this.clone.wtTable.TABLE)+4+"px"}},w.prototype=new j,w.prototype.resetFixedPosition=function(){var c,d;if(this.instance.wtTable.holder.parentNode){var e=this.clone.wtTable.holder.parentNode;if(this.scrollHandler===a){var f=this.instance.wtTable.holder.getBoundingClientRect(),g=Math.ceil(f.left),h=Math.ceil(f.right);c=0>g&&h-e.offsetWidth>0?-g+"px":"0",d=this.instance.wtTable.hider.style.top}else b.freezeOverlays||(c=this.getScrollPosition()+"px",d=this.instance.wtTable.hider.style.top);b.Dom.setOverlayPosition(e,c,d),e.style.height=b.Dom.outerHeight(this.clone.wtTable.TABLE)+"px",e.style.width=b.Dom.outerWidth(this.clone.wtTable.TABLE)+4+"px"}},w.prototype.refresh=function(a){this.applyToDOM(),j.prototype.refresh.call(this,a)},w.prototype.getScrollPosition=function(){return b.Dom.getScrollLeft(this.scrollHandler)},w.prototype.setScrollPosition=function(c){this.scrollHandler===a?a.scrollTo(c,b.Dom.getWindowScrollTop()):this.scrollHandler.scrollLeft=c},w.prototype.onScroll=function(){this.instance.getSetting("onScrollHorizontally")},w.prototype.sumCellSizes=function(a,b){for(var c=0;b>a;)c+=this.instance.wtTable.getStretchedColumnWidth(a)||this.instance.wtSettings.defaultColumnWidth,a++;return c},w.prototype.applyToDOM=function(){var a=this.instance.getSetting("totalColumns"),b=this.instance.wtViewport.getRowHeaderWidth();if(this.fixedContainer.style.width=b+this.sumCellSizes(0,a)+"px","number"==typeof this.instance.wtViewport.columnsRenderCalculator.startPosition)this.fixed.style.left=this.instance.wtViewport.columnsRenderCalculator.startPosition+"px";else{if(0!==a)throw new Error("Incorrect value of the columnsRenderCalculator");this.fixed.style.left="0"}this.fixed.style.right=""},w.prototype.scrollTo=function(a,b){var c=this.getTableParentOffset();if(b)c+=this.sumCellSizes(0,a+1),c-=this.instance.wtViewport.getViewportWidth();else{var d=this.instance.getSetting("fixedColumnsLeft");c+=this.sumCellSizes(d,a)}this.setScrollPosition(c)},w.prototype.getTableParentOffset=function(){return this.scrollHandler===a?this.instance.wtTable.holderOffset.left:0},x.prototype=new j,x.prototype.resetFixedPosition=function(){var c,d;if(this.instance.wtTable.holder.parentNode){var e=this.clone.wtTable.holder.parentNode;if(this.scrollHandler===a){var f=this.instance.wtTable.holder.getBoundingClientRect(),g=Math.ceil(f.top),h=Math.ceil(f.bottom);c=this.instance.wtTable.hider.style.left,d=0>g&&h-e.offsetHeight>0?-g+"px":"0"}else b.freezeOverlays||(d=this.getScrollPosition()+"px",c=this.instance.wtTable.hider.style.left);b.Dom.setOverlayPosition(e,c,d),e.style.width=this.instance.wtScrollbars.horizontal.scrollHandler===a?this.instance.wtViewport.getWorkspaceActualWidth()+"px":b.Dom.outerWidth(this.clone.wtTable.TABLE)+"px",e.style.height=b.Dom.outerHeight(this.clone.wtTable.TABLE)+4+"px"}},x.prototype.getScrollPosition=function(){return b.Dom.getScrollTop(this.scrollHandler)},x.prototype.setScrollPosition=function(c){this.scrollHandler===a?a.scrollTo(b.Dom.getWindowScrollLeft(),c):this.scrollHandler.scrollTop=c},x.prototype.onScroll=function(){this.instance.getSetting("onScrollVertically")},x.prototype.sumCellSizes=function(a,b){for(var c=0;b>a;)c+=this.instance.wtTable.getRowHeight(a)||this.instance.wtSettings.settings.defaultRowHeight,a++;return c},x.prototype.refresh=function(a){this.applyToDOM(),j.prototype.refresh.call(this,a)},x.prototype.applyToDOM=function(){var a=this.instance.getSetting("totalRows"),b=this.instance.wtViewport.getColumnHeaderHeight();if(this.fixedContainer.style.height=b+this.sumCellSizes(0,a)+"px","number"==typeof this.instance.wtViewport.rowsRenderCalculator.startPosition)this.fixed.style.top=this.instance.wtViewport.rowsRenderCalculator.startPosition+"px";else{if(0!==a)throw new Error("Incorrect value of the rowsRenderCalculator");this.fixed.style.top="0"}this.fixed.style.bottom=""},x.prototype.scrollTo=function(a,b){var c=this.getTableParentOffset();if(b)c+=this.sumCellSizes(0,a+1),c-=this.instance.wtViewport.getViewportHeight(),c+=1;else{var d=this.instance.getSetting("fixedRowsTop");c+=this.sumCellSizes(d,a)}this.setScrollPosition(c)},x.prototype.getTableParentOffset=function(){return this.scrollHandler===a?this.instance.wtTable.holderOffset.top:0},y.prototype.registerListeners=function(){var c=this;this.refreshAll=function(){if(c.instance.drawn){if(!c.instance.wtTable.holder.parentNode)return void c.destroy();c.instance.draw(!0),c.vertical.onScroll(),c.horizontal.onScroll()}};var d=b.eventManager(c.instance);d.addEventListener(this.vertical.scrollHandler,"scroll",this.refreshAll),this.vertical.scrollHandler!==this.horizontal.scrollHandler&&d.addEventListener(this.horizontal.scrollHandler,"scroll",this.refreshAll),this.vertical.scrollHandler!==a&&this.horizontal.scrollHandler!==a&&d.addEventListener(a,"scroll",this.refreshAll)},y.prototype.destroy=function(){var c=b.eventManager(this.instance);this.vertical&&(this.vertical.destroy(),c.removeEventListener(this.vertical.scrollHandler,"scroll",this.refreshAll)),this.horizontal&&(this.horizontal.destroy(),c.removeEventListener(this.horizontal.scrollHandler,"scroll",this.refreshAll)),c.removeEventListener(a,"scroll",this.refreshAll),this.corner&&this.corner.destroy(),this.debug&&this.debug.destroy()},y.prototype.refresh=function(a){this.horizontal&&this.horizontal.refresh(a),this.vertical&&this.vertical.refresh(a),this.corner&&this.corner.refresh(a),this.debug&&this.debug.refresh(a)},y.prototype.applyToDOM=function(){this.horizontal&&this.horizontal.applyToDOM(),this.vertical&&this.vertical.applyToDOM()},z.prototype.getBorder=function(a){return this.instanceBorders[a.guid]?this.instanceBorders[a.guid]:void(this.instanceBorders[a.guid]=new k(a,this.settings))},z.prototype.isEmpty=function(){return null===this.cellRange},z.prototype.add=function(a){this.isEmpty()?this.cellRange=new m(a,a,a):this.cellRange.expand(a)},z.prototype.replace=function(a,b){if(!this.isEmpty()){if(this.cellRange.from.isEqual(a))return this.cellRange.from=b,!0;if(this.cellRange.to.isEqual(a))return this.cellRange.to=b,!0}return!1},z.prototype.clear=function(){this.cellRange=null},z.prototype.getCorners=function(){var a=this.cellRange.getTopLeftCorner(),b=this.cellRange.getBottomRightCorner();return[a.row,a.col,b.row,b.col]},z.prototype.addClassAtCoords=function(a,c,d,e){var f=a.wtTable.getCell(new l(c,d));"object"==typeof f&&b.Dom.addClass(f,e)},z.prototype.draw=function(a){var c,d,e,f,g,h=this,i=a.wtTable.getRenderedRowsCount(),j=a.wtTable.getRenderedColumnsCount();if(this.isEmpty())return void(this.settings.border&&(f=this.getBorder(a),f&&f.disappear()));c=this.getCorners();for(var k=0;j>k;k++)e=a.wtTable.columnFilter.renderedToSource(k),e>=c[1]&&e<=c[3]&&(g=a.wtTable.getColumnHeader(e),g&&h.settings.highlightColumnClassName&&b.Dom.addClass(g,h.settings.highlightColumnClassName));for(var l=0;i>l;l++){d=a.wtTable.rowFilter.renderedToSource(l),d>=c[0]&&d<=c[2]&&(g=a.wtTable.getRowHeader(d),g&&h.settings.highlightRowClassName&&b.Dom.addClass(g,h.settings.highlightRowClassName));for(var k=0;j>k;k++)e=a.wtTable.columnFilter.renderedToSource(k),d>=c[0]&&d<=c[2]&&e>=c[1]&&e<=c[3]?h.settings.className&&h.addClassAtCoords(a,d,e,h.settings.className):d>=c[0]&&d<=c[2]?h.settings.highlightRowClassName&&h.addClassAtCoords(a,d,e,h.settings.highlightRowClassName):e>=c[1]&&e<=c[3]&&h.settings.highlightColumnClassName&&h.addClassAtCoords(a,d,e,h.settings.highlightColumnClassName)}a.getSetting("onBeforeDrawBorders",c,this.settings.className),this.settings.border&&(f=this.getBorder(a),f&&f.appear(c))},A.prototype.update=function(a,b){if(void 0===b)for(var c in a)a.hasOwnProperty(c)&&(this.settings[c]=a[c]);else this.settings[a]=b;return this.instance},A.prototype.getSetting=function(a,b,c,d,e){return"function"==typeof this.settings[a]?this.settings[a](b,c,d,e):void 0!==b&&Array.isArray(this.settings[a])?this.settings[a][b]:this.settings[a]},A.prototype.has=function(a){return!!this.settings[a]},B.prototype.isWorkingOnClone=function(){return!!this.instance.cloneSource},B.prototype.draw=function(a){if(this.isWorkingOnClone()||(this.holderOffset=b.Dom.offset(this.holder),a=this.instance.wtViewport.createRenderCalculators(a)),a)this.isWorkingOnClone()||this.instance.wtViewport.createVisibleCalculators(),this.instance.wtScrollbars&&this.instance.wtScrollbars.refresh(!0);else{this.tableOffset=this.isWorkingOnClone()?this.instance.cloneSource.wtTable.tableOffset:b.Dom.offset(this.TABLE);var c;c=this.instance.cloneOverlay instanceof q||this.instance.cloneOverlay instanceof x||this.instance.cloneOverlay instanceof v?0:this.instance.wtViewport.rowsRenderCalculator.startRow;var d;d=this.instance.cloneOverlay instanceof q||this.instance.cloneOverlay instanceof w||this.instance.cloneOverlay instanceof v?0:this.instance.wtViewport.columnsRenderCalculator.startColumn,this.rowFilter=new t(c,this.instance.getSetting("totalRows"),this.instance.getSetting("columnHeaders").length),this.columnFilter=new n(d,this.instance.getSetting("totalColumns"),this.instance.getSetting("rowHeaders").length),this._doDraw()}return this.refreshSelections(a),this.isWorkingOnClone()||(this.instance.wtScrollbars.vertical.resetFixedPosition(),this.instance.wtScrollbars.horizontal.resetFixedPosition(),this.instance.wtScrollbars.corner.resetFixedPosition()),this.instance.drawn=!0,this},B.prototype._doDraw=function(){var a=new C(this);a.render()},B.prototype.removeClassFromCells=function(a){for(var c=this.TABLE.querySelectorAll("."+a),d=0,e=c.length;e>d;d++)b.Dom.removeClass(c[d],a)},B.prototype.refreshSelections=function(a){var b,c;if(this.instance.selections){if(c=this.instance.selections.length,a)for(b=0;c>b;b++)this.instance.selections[b].settings.className&&this.removeClassFromCells(this.instance.selections[b].settings.className),this.instance.selections[b].settings.highlightRowClassName&&this.removeClassFromCells(this.instance.selections[b].settings.highlightRowClassName),this.instance.selections[b].settings.highlightColumnClassName&&this.removeClassFromCells(this.instance.selections[b].settings.highlightColumnClassName);for(b=0;c>b;b++)this.instance.selections[b].draw(this.instance,a)}},B.prototype.getCell=function(a){if(this.isRowBeforeRenderedRows(a.row))return-1;if(this.isRowAfterRenderedRows(a.row))return-2;var b=this.TBODY.childNodes[this.rowFilter.sourceToRendered(a.row)];return b?b.childNodes[this.columnFilter.sourceColumnToVisibleRowHeadedColumn(a.col)]:void 0},B.prototype.getColumnHeader=function(a,b){b||(b=0);var c=this.THEAD.childNodes[b];return c?c.childNodes[this.columnFilter.sourceColumnToVisibleRowHeadedColumn(a)]:void 0},B.prototype.getRowHeader=function(a){if(0===this.columnFilter.sourceColumnToVisibleRowHeadedColumn(0))return null;var b=this.TBODY.childNodes[this.rowFilter.sourceToRendered(a)];return b?b.childNodes[0]:void 0},B.prototype.getCoords=function(a){var c=a.parentNode,d=b.Dom.index(c);return d=c.parentNode===this.THEAD?this.rowFilter.visibleColHeadedRowToSourceRow(d):this.rowFilter.renderedToSource(d),new l(d,this.columnFilter.visibleRowHeadedColumnToSourceColumn(a.cellIndex))},B.prototype.getTrForRow=function(a){return this.TBODY.childNodes[this.rowFilter.sourceToRendered(a)]},B.prototype.getFirstRenderedRow=function(){return this.instance.wtViewport.rowsRenderCalculator.startRow},B.prototype.getFirstVisibleRow=function(){return this.instance.wtViewport.rowsVisibleCalculator.startRow},B.prototype.getFirstRenderedColumn=function(){return this.instance.wtViewport.columnsRenderCalculator.startColumn},B.prototype.getFirstVisibleColumn=function(){return this.instance.wtViewport.columnsVisibleCalculator.startColumn},B.prototype.getLastRenderedRow=function(){return this.instance.wtViewport.rowsRenderCalculator.endRow},B.prototype.getLastVisibleRow=function(){return this.instance.wtViewport.rowsVisibleCalculator.endRow},B.prototype.getLastRenderedColumn=function(){return this.instance.wtViewport.columnsRenderCalculator.endColumn},B.prototype.getLastVisibleColumn=function(){return this.instance.wtViewport.columnsVisibleCalculator.endColumn},B.prototype.isRowBeforeRenderedRows=function(a){return this.rowFilter.sourceToRendered(a)<0&&a>=0},B.prototype.isRowAfterViewport=function(a){return a>this.getLastVisibleRow()},B.prototype.isRowAfterRenderedRows=function(a){return a>this.getLastRenderedRow()},B.prototype.isColumnBeforeViewport=function(a){return this.columnFilter.sourceToRendered(a)<0&&a>=0},B.prototype.isColumnAfterViewport=function(a){return a>this.getLastVisibleColumn()},B.prototype.isLastRowFullyVisible=function(){return this.getLastVisibleRow()===this.getLastRenderedRow()},B.prototype.isLastColumnFullyVisible=function(){return this.getLastVisibleColumn()===this.getLastRenderedColumn},B.prototype.getRenderedColumnsCount=function(){return this.instance.cloneOverlay instanceof q?this.instance.getSetting("totalColumns"):this.instance.cloneOverlay instanceof w||this.instance.cloneOverlay instanceof v?this.instance.getSetting("fixedColumnsLeft"):this.instance.wtViewport.columnsRenderCalculator.count},B.prototype.getRenderedRowsCount=function(){return this.instance.cloneOverlay instanceof q?this.instance.getSetting("totalRows"):this.instance.cloneOverlay instanceof x||this.instance.cloneOverlay instanceof v?this.instance.getSetting("fixedRowsTop"):this.instance.wtViewport.rowsRenderCalculator.count},B.prototype.getVisibleRowsCount=function(){return this.instance.wtViewport.rowsVisibleCalculator.count},B.prototype.allRowsInViewport=function(){return this.instance.getSetting("totalRows")==this.getVisibleRowsCount()},B.prototype.getRowHeight=function(a){var b=this.instance.wtSettings.settings.rowHeight(a),c=this.instance.wtViewport.oversizedRows[a];return void 0!==c&&(b=b?Math.max(b,c):c),b},B.prototype.getColumnHeaderHeight=function(a){var b=this.instance.wtSettings.settings.defaultRowHeight,c=this.instance.wtViewport.oversizedColumnHeaders[a];return void 0!==c&&(b=b?Math.max(b,c):c),b},B.prototype.getVisibleColumnsCount=function(){return this.instance.wtViewport.columnsVisibleCalculator.count},B.prototype.allColumnsInViewport=function(){return this.instance.getSetting("totalColumns")==this.getVisibleColumnsCount()},B.prototype.getColumnWidth=function(a){var b=this.instance.wtSettings.settings.columnWidth;"function"==typeof b?b=b(a):"object"==typeof b&&(b=b[a]);var c=this.instance.wtViewport.oversizedCols[a];return void 0!==c&&(b=b?Math.max(b,c):c),b},B.prototype.getStretchedColumnWidth=function(a){var b,c=this.getColumnWidth(a)||this.instance.wtSettings.settings.defaultColumnWidth,d=this.instance.wtViewport.columnsRenderCalculator;return d&&(b=d.getStretchedColumnWidth(a,c),b&&(c=b)),c},C.prototype.render=function(){this.wtTable.isWorkingOnClone()||this.instance.getSetting("beforeDraw",!0),this.rowHeaders=this.instance.getSetting("rowHeaders"),this.rowHeaderCount=this.rowHeaders.length,this.fixedRowsTop=this.instance.getSetting("fixedRowsTop"),this.columnHeaders=this.instance.getSetting("columnHeaders"),this.columnHeaderCount=this.columnHeaders.length; +var a,b,c=this.instance.getSetting("totalRows"),d=this.instance.getSetting("totalColumns"),e=!1,f=this.wtTable.getRenderedRowsCount();if(d>0&&(this.adjustAvailableNodes(),e=!0,this.renderColGroups(),this.renderColumnHeaders(),a=this.wtTable.getRenderedColumnsCount(),this.renderRows(c,f,a),this.wtTable.isWorkingOnClone()?this.adjustColumnHeaderHeights():(b=this.instance.wtViewport.getWorkspaceWidth(),this.instance.wtViewport.containerWidth=null),this.adjustColumnWidths(a)),e||this.adjustAvailableNodes(),this.removeRedundantRows(f),!this.wtTable.isWorkingOnClone()){if(this.markOversizedRows(),this.instance.wtViewport.createVisibleCalculators(),this.instance.wtScrollbars.applyToDOM(),b!==this.instance.wtViewport.getWorkspaceWidth()){this.instance.wtViewport.containerWidth=null;for(var g=this.wtTable.getFirstRenderedColumn(),h=this.wtTable.getLastRenderedColumn(),i=g;h>i;i++){var j=this.wtTable.getStretchedColumnWidth(i),k=this.columnFilter.sourceToRendered(i);this.COLGROUP.childNodes[k+this.rowHeaderCount].style.width=j+"px"}}this.instance.wtScrollbars.refresh(!1),this.instance.getSetting("onDraw",!0)}},C.prototype.removeRedundantRows=function(a){for(;this.wtTable.tbodyChildrenLength>a;)this.TBODY.removeChild(this.TBODY.lastChild),this.wtTable.tbodyChildrenLength--},C.prototype.renderRows=function(a,b,c){for(var d,e,f=0,g=this.rowFilter.renderedToSource(f),h=this.wtTable.isWorkingOnClone();a>g&&g>=0;){if(f>1e3)throw new Error("Security brake: Too much TRs. Please define height for your table, which will enforce scrollbars.");if(void 0!==b&&f===b)break;if(e=this.getOrCreateTrForRow(f,e),this.renderRowHeaders(g,e),this.adjustColumns(e,c+this.rowHeaderCount),d=this.renderCells(g,e,c),h||this.resetOversizedRow(g),e.firstChild){var i=this.instance.wtTable.getRowHeight(g);e.firstChild.style.height=i?i+"px":""}f++,g=this.rowFilter.renderedToSource(f)}},C.prototype.resetOversizedRow=function(a){this.instance.wtViewport.oversizedRows&&this.instance.wtViewport.oversizedRows[a]&&(this.instance.wtViewport.oversizedRows[a]=void 0)},C.prototype.markOversizedRows=function(){for(var a,c,d,e,f=this.instance.wtTable.TBODY.childNodes.length;f;)f--,d=this.instance.wtTable.rowFilter.renderedToSource(f),a=this.instance.wtTable.getRowHeight(d),e=this.instance.wtTable.getTrForRow(d),c=b.Dom.innerHeight(e)-1,(!a&&this.instance.wtSettings.settings.defaultRowHeighta)&&(this.instance.wtViewport.oversizedRows[d]=c)},C.prototype.adjustColumnHeaderHeights=function(){for(var a=this.instance.getSetting("columnHeaders"),b=0,c=a.length;c>b;b++)if(this.instance.wtViewport.oversizedColumnHeaders[b]){if(0===this.instance.wtTable.THEAD.childNodes[b].childNodes.length)return;this.instance.wtTable.THEAD.childNodes[b].childNodes[0].style.height=this.instance.wtViewport.oversizedColumnHeaders[b]+"px"}},C.prototype.markIfOversizedColumnHeader=function(a){var c,d,e,f,g=(0!==this.instance.wtTable.THEAD.childNodes.length?this.instance.wtTable.THEAD.childNodes[0].childNodes.length:0,this.instance.getSetting("columnHeaders")),h=g.length,i=h;for(c=this.instance.wtTable.columnFilter.renderedToSource(a);i;)i--,d=this.instance.wtTable.getColumnHeaderHeight(i),e=this.instance.wtTable.getColumnHeader(c,i),e&&(f=b.Dom.innerHeight(e)-1,(!d&&this.instance.wtSettings.settings.defaultRowHeightd)&&(this.instance.wtViewport.oversizedColumnHeaders[i]=f))},C.prototype.renderCells=function(a,c,d){for(var e,f,g=0;d>g;g++)f=this.columnFilter.renderedToSource(g),e=0===g?c.childNodes[this.columnFilter.sourceColumnToVisibleRowHeadedColumn(f)]:e.nextSibling,"TH"==e.nodeName&&(e=this.utils.replaceThWithTd(e,c)),b.Dom.hasClass(e,"hide")||(e.className=""),e.removeAttribute("style"),this.instance.wtSettings.settings.cellRenderer(a,f,e);return e},C.prototype.adjustColumnWidths=function(a){var b;this.instance.wtViewport.columnsRenderCalculator.refreshStretching(this.instance.wtViewport.getViewportWidth());for(var c=0;a>c;c++)b=this.wtTable.getStretchedColumnWidth(this.columnFilter.renderedToSource(c)),this.COLGROUP.childNodes[c+this.rowHeaderCount].style.width=b+"px"},C.prototype.appendToTbody=function(a){this.TBODY.appendChild(a),this.wtTable.tbodyChildrenLength++},C.prototype.getOrCreateTrForRow=function(a,b){var c;return a>=this.wtTable.tbodyChildrenLength?(c=this.createRow(),this.appendToTbody(c)):c=0===a?this.TBODY.firstChild:b.nextSibling,c},C.prototype.createRow=function(){for(var a=document.createElement("TR"),b=0;bb;b++){var e=this.columnFilter.renderedToSource(b);this.renderColumnHeader(d,e,a.childNodes[b+this.rowHeaderCount]),this.wtTable.isWorkingOnClone()||this.markIfOversizedColumnHeader(b)}},C.prototype.adjustColGroups=function(){for(var a=this.wtTable.getRenderedColumnsCount();this.wtTable.colgroupChildrenLengtha+this.rowHeaderCount;)this.COLGROUP.removeChild(this.COLGROUP.lastChild),this.wtTable.colgroupChildrenLength--},C.prototype.adjustThead=function(){var a=this.wtTable.getRenderedColumnsCount(),c=this.THEAD.firstChild;if(this.columnHeaders.length){for(var d=0,e=this.columnHeaders.length;e>d;d++){for(c=this.THEAD.childNodes[d],c||(c=document.createElement("TR"),this.THEAD.appendChild(c)),this.theadChildrenLength=c.childNodes.length;this.theadChildrenLengtha+this.rowHeaderCount;)c.removeChild(c.lastChild),this.theadChildrenLength--}var f=this.THEAD.childNodes.length;if(f>this.columnHeaders.length)for(var d=this.columnHeaders.length;f>d;d++)this.THEAD.removeChild(this.THEAD.lastChild)}else c&&b.Dom.empty(c)},C.prototype.getTrForColumnHeaders=function(a){var b=this.THEAD.childNodes[a];return b},C.prototype.renderColumnHeader=function(a,b,c){return c.className="",c.removeAttribute("style"),this.columnHeaders[a](b,c,a)},C.prototype.renderColGroups=function(){for(var a=0;ac;){var d=document.createElement("TD");a.appendChild(d),c++}for(;c>b;)a.removeChild(a.lastChild),c--},C.prototype.removeRedundantColumns=function(a){for(;this.wtTable.tbodyChildrenLength>a;)this.TBODY.removeChild(this.TBODY.lastChild),this.wtTable.tbodyChildrenLength--},C.utils={},C.utils.replaceTdWithTh=function(a,b){var c;return c=document.createElement("TH"),b.insertBefore(c,a),b.removeChild(a),c},C.utils.replaceThWithTd=function(a,b){var c=document.createElement("TD");return b.insertBefore(c,a),b.removeChild(a),c},D.prototype.getWorkspaceHeight=function(){var c=this.instance.wtScrollbars.vertical.scrollHandler;if(c===a)return document.documentElement.clientHeight;var d=b.Dom.outerHeight(c),e=d>0&&c.clientHeight>0?c.clientHeight:1/0;return e},D.prototype.getWorkspaceWidth=function(){var c,d,e=this.instance.getSetting("totalColumns"),f=this.instance.wtScrollbars.horizontal.scrollHandler,g=this.instance.getSetting("stretchH");return c=b.freezeOverlays?Math.min(document.documentElement.offsetWidth-this.getWorkspaceOffset().left,document.documentElement.offsetWidth):Math.min(this.getContainerFillWidth(),document.documentElement.offsetWidth-this.getWorkspaceOffset().left,document.documentElement.offsetWidth),f===a&&e>0&&this.sumColumnWidths(0,e-1)>c?document.documentElement.clientWidth:f!==a&&(d=this.instance.wtScrollbars.horizontal.scrollHandler.style.overflow,"scroll"==d||"hidden"==d||"auto"==d)?Math.max(c,f.clientWidth):"none"!==g&&g?c:Math.max(c,b.Dom.outerWidth(this.instance.wtTable.TABLE))},D.prototype.sumColumnWidths=function(a,b){for(var c=0;b>a;)c+=this.instance.wtTable.getColumnWidth(a)||this.instance.wtSettings.defaultColumnWidth,a++;return c},D.prototype.getContainerFillWidth=function(){if(this.containerWidth)return this.containerWidth;for(var a,b,c=this.instance.wtTable.holder;c.parentNode!=document.body&&null!=c.parentNode&&-1===c.className.indexOf("handsontable");)c=c.parentNode;return b=document.createElement("DIV"),b.style.width="100%",b.style.height="1px",c.appendChild(b),a=b.offsetWidth,this.containerWidth=a,c.removeChild(b),a},D.prototype.getWorkspaceOffset=function(){return b.Dom.offset(this.instance.wtTable.TABLE)},D.prototype.getWorkspaceActualHeight=function(){return b.Dom.outerHeight(this.instance.wtTable.TABLE)},D.prototype.getWorkspaceActualWidth=function(){return b.Dom.outerWidth(this.instance.wtTable.TABLE)||b.Dom.outerWidth(this.instance.wtTable.TBODY)||b.Dom.outerWidth(this.instance.wtTable.THEAD)},D.prototype.getColumnHeaderHeight=function(){return isNaN(this.columnHeaderHeight)&&(this.columnHeaderHeight=b.Dom.outerHeight(this.instance.wtTable.THEAD)),this.columnHeaderHeight},D.prototype.getViewportHeight=function(){var a=this.getWorkspaceHeight();if(1/0===a)return a;var b=this.getColumnHeaderHeight();return b>0&&(a-=b),a},D.prototype.getRowHeaderWidth=function(){if(this.instance.cloneSource)return this.instance.cloneSource.wtViewport.getRowHeaderWidth();if(isNaN(this.rowHeaderWidth)){var a=this.instance.getSetting("rowHeaders");if(a.length){var c=this.instance.wtTable.TABLE.querySelector("TH");this.rowHeaderWidth=0;for(var d=0,e=a.length;e>d;d++)c?(this.rowHeaderWidth+=b.Dom.outerWidth(c),c=c.nextSibling):this.rowHeaderWidth+=50}else this.rowHeaderWidth=0}return this.rowHeaderWidth},D.prototype.getViewportWidth=function(){var a,b=this.getWorkspaceWidth();return 1/0===b?b:(a=this.getRowHeaderWidth(),a>0?b-a:b)},D.prototype.createRowsCalculator=function(a){this.rowHeaderWidth=0/0;var b;b=this.instance.wtSettings.settings.renderAllRows?1/0:this.getViewportHeight();var c=this.instance.wtScrollbars.vertical.getScrollPosition()-this.instance.wtScrollbars.vertical.getTableParentOffset();0>c&&(c=0);var d=this.instance.getSetting("fixedRowsTop");if(d){var e=this.instance.wtScrollbars.vertical.sumCellSizes(0,d);c+=e,b-=e}var f=this;return new F(b,c,this.instance.getSetting("totalRows"),function(a){return f.instance.wtTable.getRowHeight(a)},a?null:this.instance.wtSettings.settings.viewportRowCalculatorOverride,a?!0:!1)},D.prototype.createColumnsCalculator=function(a){this.columnHeaderHeight=0/0;var b=this.getViewportWidth(),c=this.instance.wtScrollbars.horizontal.getScrollPosition()-this.instance.wtScrollbars.vertical.getTableParentOffset();0>c&&(c=0);var d=this.instance.getSetting("fixedColumnsLeft");if(d){var e=this.instance.wtScrollbars.horizontal.sumCellSizes(0,d);c+=e,b-=e}var f=this;return new E(b,c,this.instance.getSetting("totalColumns"),function(a){return f.instance.wtTable.getColumnWidth(a)},a?null:this.instance.wtSettings.settings.viewportColumnCalculatorOverride,a?!0:!1,this.instance.getSetting("stretchH"))},D.prototype.createRenderCalculators=function(a){if(a){var b=this.createRowsCalculator(!0),c=this.createColumnsCalculator(!0);this.areAllProposedVisibleRowsAlreadyRendered(b)&&this.areAllProposedVisibleColumnsAlreadyRendered(c)||(a=!1)}return a||(this.rowsRenderCalculator=this.createRowsCalculator(),this.columnsRenderCalculator=this.createColumnsCalculator()),this.rowsVisibleCalculator=null,this.columnsVisibleCalculator=null,a},D.prototype.createVisibleCalculators=function(){this.rowsVisibleCalculator=this.createRowsCalculator(!0),this.columnsVisibleCalculator=this.createColumnsCalculator(!0)},D.prototype.areAllProposedVisibleRowsAlreadyRendered=function(a){return this.rowsVisibleCalculator?a.startRow0?!1:a.endRow>this.rowsRenderCalculator.endRow||a.endRow===this.rowsRenderCalculator.endRow&&a.endRow0?!1:a.endColumn>this.columnsRenderCalculator.endColumn||a.endColumn===this.columnsRenderCalculator.endColumn&&a.endColumn1)for(b=1,d=arguments.length;d>b;b++)e.push(arguments[b]);if(i){if("undefined"==typeof i[a])throw new Error("Handsontable do not provide action: "+a);f=i[a].apply(i,e),"destroy"===a&&h.removeData()}return f}}(a,jQuery,b)}(window,Handsontable),/*! + * numeral.js + * version : 1.5.3 + * author : Adam Draper + * license : MIT + * http://adamwdraper.github.com/Numeral-js/ + */ +function(){function a(a){this._value=a}function b(a,b,c,d){var e,f,g=Math.pow(10,b);return f=(c(a*g)/g).toFixed(b),d&&(e=new RegExp("0{1,"+d+"}$"),f=f.replace(e,"")),f}function c(a,b,c){var d;return d=b.indexOf("$")>-1?e(a,b,c):b.indexOf("%")>-1?f(a,b,c):b.indexOf(":")>-1?g(a,b):i(a._value,b,c)}function d(a,b){var c,d,e,f,g,i=b,j=["KB","MB","GB","TB","PB","EB","ZB","YB"],k=!1;if(b.indexOf(":")>-1)a._value=h(b);else if(b===q)a._value=0;else{for("."!==o[p].delimiters.decimal&&(b=b.replace(/\./g,"").replace(o[p].delimiters.decimal,".")),c=new RegExp("[^a-zA-Z]"+o[p].abbreviations.thousand+"(?:\\)|(\\"+o[p].currency.symbol+")?(?:\\))?)?$"),d=new RegExp("[^a-zA-Z]"+o[p].abbreviations.million+"(?:\\)|(\\"+o[p].currency.symbol+")?(?:\\))?)?$"),e=new RegExp("[^a-zA-Z]"+o[p].abbreviations.billion+"(?:\\)|(\\"+o[p].currency.symbol+")?(?:\\))?)?$"),f=new RegExp("[^a-zA-Z]"+o[p].abbreviations.trillion+"(?:\\)|(\\"+o[p].currency.symbol+")?(?:\\))?)?$"),g=0;g<=j.length&&!(k=b.indexOf(j[g])>-1?Math.pow(1024,g+1):!1);g++);a._value=(k?k:1)*(i.match(c)?Math.pow(10,3):1)*(i.match(d)?Math.pow(10,6):1)*(i.match(e)?Math.pow(10,9):1)*(i.match(f)?Math.pow(10,12):1)*(b.indexOf("%")>-1?.01:1)*((b.split("-").length+Math.min(b.split("(").length-1,b.split(")").length-1))%2?1:-1)*Number(b.replace(/[^0-9\.]+/g,"")),a._value=k?Math.ceil(a._value):a._value}return a._value}function e(a,b,c){var d,e,f=b.indexOf("$"),g=b.indexOf("("),h=b.indexOf("-"),j="";return b.indexOf(" $")>-1?(j=" ",b=b.replace(" $","")):b.indexOf("$ ")>-1?(j=" ",b=b.replace("$ ","")):b=b.replace("$",""),e=i(a._value,b,c),1>=f?e.indexOf("(")>-1||e.indexOf("-")>-1?(e=e.split(""),d=1,(g>f||h>f)&&(d=0),e.splice(d,0,o[p].currency.symbol+j),e=e.join("")):e=o[p].currency.symbol+j+e:e.indexOf(")")>-1?(e=e.split(""),e.splice(-1,0,j+o[p].currency.symbol),e=e.join("")):e=e+j+o[p].currency.symbol,e}function f(a,b,c){var d,e="",f=100*a._value;return b.indexOf(" %")>-1?(e=" ",b=b.replace(" %","")):b=b.replace("%",""),d=i(f,b,c),d.indexOf(")")>-1?(d=d.split(""),d.splice(-1,0,e+"%"),d=d.join("")):d=d+e+"%",d}function g(a){var b=Math.floor(a._value/60/60),c=Math.floor((a._value-60*b*60)/60),d=Math.round(a._value-60*b*60-60*c);return b+":"+(10>c?"0"+c:c)+":"+(10>d?"0"+d:d)}function h(a){var b=a.split(":"),c=0;return 3===b.length?(c+=60*Number(b[0])*60,c+=60*Number(b[1]),c+=Number(b[2])):2===b.length&&(c+=60*Number(b[0]),c+=Number(b[1])),Number(c)}function i(a,c,d){var e,f,g,h,i,j,k=!1,l=!1,m=!1,n="",r=!1,s=!1,t=!1,u=!1,v=!1,w="",x="",y=Math.abs(a),z=["B","KB","MB","GB","TB","PB","EB","ZB","YB"],A="",B=!1;if(0===a&&null!==q)return q;if(c.indexOf("(")>-1?(k=!0,c=c.slice(1,-1)):c.indexOf("+")>-1&&(l=!0,c=c.replace(/\+/g,"")),c.indexOf("a")>-1&&(r=c.indexOf("aK")>=0,s=c.indexOf("aM")>=0,t=c.indexOf("aB")>=0,u=c.indexOf("aT")>=0,v=r||s||t||u,c.indexOf(" a")>-1?(n=" ",c=c.replace(" a","")):c=c.replace("a",""),y>=Math.pow(10,12)&&!v||u?(n+=o[p].abbreviations.trillion,a/=Math.pow(10,12)):y=Math.pow(10,9)&&!v||t?(n+=o[p].abbreviations.billion,a/=Math.pow(10,9)):y=Math.pow(10,6)&&!v||s?(n+=o[p].abbreviations.million,a/=Math.pow(10,6)):(y=Math.pow(10,3)&&!v||r)&&(n+=o[p].abbreviations.thousand,a/=Math.pow(10,3))),c.indexOf("b")>-1)for(c.indexOf(" b")>-1?(w=" ",c=c.replace(" b","")):c=c.replace("b",""),g=0;g<=z.length;g++)if(e=Math.pow(1024,g),f=Math.pow(1024,g+1),a>=e&&f>a){w+=z[g],e>0&&(a/=e);break}return c.indexOf("o")>-1&&(c.indexOf(" o")>-1?(x=" ",c=c.replace(" o","")):c=c.replace("o",""),x+=o[p].ordinal(a)),c.indexOf("[.]")>-1&&(m=!0,c=c.replace("[.]",".")),h=a.toString().split(".")[0],i=c.split(".")[1],j=c.indexOf(","),i?(i.indexOf("[")>-1?(i=i.replace("]",""),i=i.split("["),A=b(a,i[0].length+i[1].length,d,i[1].length)):A=b(a,i.length,d),h=A.split(".")[0],A=A.split(".")[1].length?o[p].delimiters.decimal+A.split(".")[1]:"",m&&0===Number(A.slice(1))&&(A="")):h=b(a,null,d),h.indexOf("-")>-1&&(h=h.slice(1),B=!0),j>-1&&(h=h.toString().replace(/(\d)(?=(\d{3})+(?!\d))/g,"$1"+o[p].delimiters.thousands)),0===c.indexOf(".")&&(h=""),(k&&B?"(":"")+(!k&&B?"-":"")+(!B&&l?"+":"")+h+A+(x?x:"")+(n?n:"")+(w?w:"")+(k&&B?")":"")}function j(a,b){o[a]=b}function k(a){var b=a.toString().split(".");return b.length<2?1:Math.pow(10,b[1].length)}function l(){var a=Array.prototype.slice.call(arguments);return a.reduce(function(a,b){var c=k(a),d=k(b);return c>d?c:d},-1/0)}var m,n="1.5.3",o={},p="en",q=null,r="0,0",s="undefined"!=typeof module&&module.exports;m=function(b){return m.isNumeral(b)?b=b.value():0===b||"undefined"==typeof b?b=0:Number(b)||(b=m.fn.unformat(b)),new a(Number(b))},m.version=n,m.isNumeral=function(b){return b instanceof a},m.language=function(a,b){if(!a)return p;if(a=a.toLowerCase(),a&&!b){if(!o[a])throw new Error("Unknown language : "+a);p=a}return(b||!o[a])&&j(a,b),m},m.languageData=function(a){if(!a)return o[p];if(!o[a])throw new Error("Unknown language : "+a);return o[a]},m.language("en",{delimiters:{thousands:",",decimal:"."},abbreviations:{thousand:"k",million:"m",billion:"b",trillion:"t"},ordinal:function(a){var b=a%10;return 1===~~(a%100/10)?"th":1===b?"st":2===b?"nd":3===b?"rd":"th"},currency:{symbol:"$"}}),m.zeroFormat=function(a){q="string"==typeof a?a:null},m.defaultFormat=function(a){r="string"==typeof a?a:"0.0"},m.validate=function(a,b){var c,d,e,f,g,h,i,j;if("string"!=typeof a&&(a+="",console.warn&&console.warn("Numeral.js: Value is not string. It has been co-erced to: ",a)),a=a.trim(),""===a)return!1;a=a.replace(/^[+-]?/,"");try{i=m.languageData(b)}catch(k){i=m.languageData(m.language())}return e=i.currency.symbol,g=i.abbreviations,c=i.delimiters.decimal,d="."===i.delimiters.thousands?"\\.":i.delimiters.thousands,j=a.match(/^[^\d]+/),null!==j&&(a=a.substr(1),j[0]!==e)?!1:(j=a.match(/[^\d]+$/),null!==j&&(a=a.slice(0,-1),j[0]!==g.thousand&&j[0]!==g.million&&j[0]!==g.billion&&j[0]!==g.trillion)?!1:a.match(/^\d+$/)?!0:(h=new RegExp(d+"{2}"),a.match(/[^\d.,]/g)?!1:(f=a.split(c),f.length>2?!1:f.length<2?!!f[0].match(/^\d+.*\d$/)&&!f[0].match(h):1===f[0].length?!!f[0].match(/^\d+$/)&&!f[0].match(h)&&!!f[1].match(/^\d+$/):!!f[0].match(/^\d+.*\d$/)&&!f[0].match(h)&&!!f[1].match(/^\d+$/))))},"function"!=typeof Array.prototype.reduce&&(Array.prototype.reduce=function(a,b){"use strict";if(null===this||"undefined"==typeof this)throw new TypeError("Array.prototype.reduce called on null or undefined");if("function"!=typeof a)throw new TypeError(a+" is not a function");var c,d,e=this.length>>>0,f=!1;for(1c;++c)this.hasOwnProperty(c)&&(f?d=a(d,this[c],c,this):(d=this[c],f=!0));if(!f)throw new TypeError("Reduce of empty array with no initial value");return d}),m.fn=a.prototype={clone:function(){return m(this)},format:function(a,b){return c(this,a?a:r,void 0!==b?b:Math.round)},unformat:function(a){return"[object Number]"===Object.prototype.toString.call(a)?a:d(this,a?a:r)},value:function(){return this._value},valueOf:function(){return this._value},set:function(a){return this._value=Number(a),this},add:function(a){function b(a,b){return a+c*b}var c=l.call(null,this._value,a);return this._value=[this._value,a].reduce(b,0)/c,this},subtract:function(a){function b(a,b){return a-c*b}var c=l.call(null,this._value,a);return this._value=[a].reduce(b,this._value*c)/c,this},multiply:function(a){function b(a,b){var c=l(a,b);return a*c*b*c/(c*c)}return this._value=[this._value,a].reduce(b,1),this},divide:function(a){function b(a,b){var c=l(a,b);return a*c/(b*c)}return this._value=[this._value,a].reduce(b),this},difference:function(a){return Math.abs(m(this._value).subtract(a).value())}},s&&(module.exports=m),"undefined"==typeof ender&&(this.numeral=m),"function"==typeof define&&define.amd&&define([],function(){return m})}.call(this); \ No newline at end of file diff --git a/bower_components/handsontable/dist/handsontable.js b/bower_components/handsontable/dist/handsontable.js new file mode 100644 index 0000000..79d14a0 --- /dev/null +++ b/bower_components/handsontable/dist/handsontable.js @@ -0,0 +1,20377 @@ +/*! + * Handsontable 0.12.4 + * Handsontable is a JavaScript library 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: Fri Jan 23 2015 10:07:24 GMT+0100 (CET) + */ +/*jslint white: true, browser: true, plusplus: true, indent: 4, maxerr: 50 */ + +//var Handsontable = { //class namespace +// plugins: {}, //plugin namespace +// helper: {} //helper namespace +//}; + +var Handsontable = function (rootElement, userSettings) { + userSettings = userSettings || {}; + var instance = new Handsontable.Core(rootElement, userSettings); + instance.init(); + return instance; +}; +Handsontable.plugins = {}; + +(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; + } + }; +} + +if (!Array.isArray) { + Array.isArray = function(obj) { + return toString.call(obj) == '[object Array]'; + }; +} + +// From https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/keys +// License CC-BY-SA v2.5 +if (!Object.keys) { + Object.keys = (function() { + 'use strict'; + var hasOwnProperty = Object.prototype.hasOwnProperty, + hasDontEnumBug = !({ toString: null }).propertyIsEnumerable('toString'), + dontEnums = [ + 'toString', + 'toLocaleString', + 'valueOf', + 'hasOwnProperty', + 'isPrototypeOf', + 'propertyIsEnumerable', + 'constructor' + ], + dontEnumsLength = dontEnums.length; + + return function(obj) { + if (typeof obj !== 'object' && (typeof obj !== 'function' || obj === null)) { + throw new TypeError('Object.keys called on non-object'); + } + + var result = [], prop, i; + + for (prop in obj) { + if (hasOwnProperty.call(obj, prop)) { + result.push(prop); + } + } + + if (hasDontEnumBug) { + for (i = 0; i < dontEnumsLength; i++) { + if (hasOwnProperty.call(obj, dontEnums[i])) { + result.push(dontEnums[i]); + } + } + } + return result; + }; + }()); +} + +/* + * 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 DOM 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 () {} + , eventManager = Handsontable.eventManager(instance); + + 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.insertBefore(this.container, rootElement.firstChild); + + this.guid = 'ht_' + Handsontable.helper.randomString(); //this is the namespace for global events + + if (!this.rootElement.id || this.rootElement.id.substring(0, 3) === "ht_") { + this.rootElement.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": + // //column order may have changes, so we need to translate the selection column index -> source array index + // index = instance.runHooksAndReturn('modifyCol', index); + delta = datamap.createCol(index, amount); + + if (delta) { + + if(Array.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": + //column order may have changes, so we need to translate the selection column index -> source array index + index = instance.runHooks('modifyCol', index); + + 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(Array.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; + + /* jshint ignore:start */ + default: + throw new Error('There is no such action "' + action + '"'); + break; + /* jshint ignore:end */ + } + + 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"] + * @param {String} direction (left|right|up|down) + * @param {Array} deltas array + * @return {Object|undefined} ending td in pasted area (only if any cell was changed) + */ + populateFromArray: function (start, input, end, source, method, direction, deltas) { + 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; + + /* jshint ignore:start */ + case 'overwrite': + default: + /* jshint ignore:end */ + // overwrite and other not specified options + current.row = start.row; + current.col = start.col; + + var iterators = {row: 0, col: 0}, // number of packages + selected = { // selected range + row: (end && start) ? (end.row - start.row + 1) : 1, + col: (end && start) ? (end.col - start.col + 1) : 1 + }; + + if (['up', 'left'].indexOf(direction) !== -1) { + iterators = { + row: Math.ceil(selected.row / rlen) || 1, + col: Math.ceil(selected.col / input[0].length) || 1 + }; + } else if (['down', 'right'].indexOf(direction) !== -1) { + iterators = { + row: 1, + col: 1 + }; + } + + + for (r = 0; r < rlen; r++) { + if ((end && current.row > end.row) || (!priv.settings.allowInsertRow && 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.allowInsertColumn && current.col > instance.countCols() - 1) || (current.col >= priv.settings.maxCols)) { + break; + } + + if (!instance.getCellMeta(current.row, current.col).readOnly) { + var result, + value = input[r][c], + index = { + row: r, + col: c + }; + + if (source === 'autofill') { + result = instance.runHooks('beforeAutofillInsidePopulate', index, direction, input, deltas, iterators, selected); + + if (result) { + iterators = typeof(result.iterators) !== 'undefined' ? result.iterators : iterators; + value = typeof(result.value) !== 'undefined' ? result.value : value; + } + } + + setData.push([current.row, current.col, value]); + } + + current.col++; + + if (end && c === clen - 1) { + c = -1; + + if (['down', 'right'].indexOf(direction) !== -1) { + iterators.col++; + } else if (['up', 'left'].indexOf(direction) !== -1) { + if (iterators.col > 1) { + iterators.col--; + } + } + + } + } + + current.row++; + iterators.col = 1; + + if (end && r === rlen - 1) { + r = -1; + + if (['down', 'right'].indexOf(direction) !== -1) { + iterators.row++; + } else if (['up', 'left'].indexOf(direction) !== -1) { + if (iterators.row > 1) { + iterators.row--; + } + } + + } + } + instance.setDataAtCell(setData, null, null, source || 'populateFromArray'); + break; + } + } + }; + + this.selection = selection = { //this public assignment is only temporary + inProgress: false, + + selectedHeader: { + cols: false, + rows: false + }, + + setSelectedHeaders: function (rows, cols) { + instance.selection.selectedHeader.rows = rows; + instance.selection.selectedHeader.cols = cols; + }, + + /** + * 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, keepEditorOpened) { + Handsontable.hooks.run(instance, "beforeSetRangeStart", coords); + priv.selRange = new WalkontableCellRange(coords, coords, coords); + selection.setRangeEnd(coords, null, keepEditorOpened); + }, + + /** + * 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, keepEditorOpened) { + //trigger handlers + Handsontable.hooks.run(instance, "beforeSetRangeEnd", coords); + + instance.selection.begin(); + + priv.selRange.to = new WalkontableCellCoords(coords.row, coords.col); + 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()) { + if(priv.selRange.from) { + instance.view.scrollViewport(priv.selRange.from); + } else { + instance.view.scrollViewport(coords); + } + + } + selection.refreshBorders(null, keepEditorOpened); + }, + + /** + * 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 () { + var isMultiple = !(priv.selRange.to.col === priv.selRange.from.col && priv.selRange.to.row === priv.selRange.from.row) + , modifier = Handsontable.hooks.run(instance, 'afterIsMultipleSelection', isMultiple); + + if(isMultiple) { + return modifier; + } + }, + + /** + * Selects cell relative to current cell (if possible) + */ + transformStart: function (rowDelta, colDelta, force, keepEditorOpened) { + var delta = new WalkontableCellCoords(rowDelta, colDelta); + instance.runHooks('modifyTransformStart', delta); + + /* jshint ignore:start */ + 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; + } + /* jshint ignore:end */ + + 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, keepEditorOpened); + }, + + /** + * 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'); + + if(Handsontable.mobileBrowser) { + Handsontable.Dom.addClass(instance.rootElement, 'mobile'); + } + + 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 () { + /* jshint ignore:start */ + if (this.validatorsInQueue == 0 && resolved == false) { + resolved = true; + this.onQueueEmpty(); + } + /* jshint ignore:end */ + } + }; + } + + 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]); + // column order may have changes, so we need to translate physical col index (stored in datasource) to logical (displayed to user) + var logicalCol = instance.runHooks('modifyCol', col); + 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]) || cellProperties.format )) { + var len = changes[i][3].length; + if (typeof cellProperties.language == 'undefined') { + numeral.language('en'); + } + // this input in format XXXX.XX is likely to come from paste. Let's parse it using international rules + else if (changes[i][3].indexOf(".") === len - 3 && changes[i][3].indexOf(",") === -1) { + numeral.language('en'); + } + else { + numeral.language(cellProperties.language); + } + if (numeral.validate(changes[i][3])) { + changes[i][3] = numeral().unformat(changes[i][3]); + } + } + } + + /* jshint ignore:start */ + 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); + } + /* jshint ignore:end */ + } + } + waitingForValidator.checkIfQueueIsEmpty(); + + function resolve() { + var beforeChangeResult; + + if (changes.length) { + beforeChangeResult = Handsontable.hooks.run(instance, "beforeChange", changes, source); + if (typeof beforeChangeResult === 'function') { + console.warn("Your beforeChange callback returns a function. It's not supported since Handsontable 0.12.1 (and the returned function will not be executed)."); + } else if (beforeChangeResult === false) { + changes.splice(0, changes.length); //invalidate all changes (remove everything from array) + } + } + 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.allowInsertRow) { + while (changes[i][0] > instance.countRows() - 1) { + datamap.createRow(); + } + } + + if (instance.dataType === 'array' && priv.settings.allowInsertColumn) { + 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.run(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) { + valid = Handsontable.hooks.run(instance, "afterValidate", valid, value, cellProperties.row, cellProperties.prop, source); + cellProperties.valid = valid; + + callback(valid); + Handsontable.hooks.run(instance, "postAfterValidate", valid, value, cellProperties.row, cellProperties.prop, source); + }); + }, 0)); + + } else { + //resolve callback even if validator function was not found + cellProperties.valid = true; + callback(true); + } + }; + + function setDataInputToArray(row, propOrCol, value) { + if (typeof row === "object") { //is it an array of changes + return row; + } + else { + return [ + [row, propOrCol, 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"] + * @param {String} direction edit (left|right|up|down) + * @param {Array} deltas array + * @return {Object|undefined} ending td in pasted area (only if any cell was changed) + */ + this.populateFromArray = function (row, col, input, endRow, endCol, source, method, direction, deltas) { + var c; + + 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 + } + c = typeof endRow === 'number' ? new WalkontableCellCoords(endRow, endCol) : null; + + return grid.populateFromArray(new WalkontableCellCoords(row, col), input, c, source, method, direction, deltas); + }; + + /** + * 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/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/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 (Array.isArray(priv.settings.dataSchema) || Array.isArray(data[0])) { + 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' || Array.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') { + /* jshint -W089 */ + 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) { + Handsontable.Dom.removeClass(instance.rootElement,GridSettings.prototype.className); +// instance.rootElement.removeClass(GridSettings.prototype.className); + } + if (settings.className) { + Handsontable.Dom.addClass(instance.rootElement,settings.className); +// instance.rootElement.addClass(settings.className); + } + } + + if (typeof settings.height != 'undefined'){ + var height = settings.height; + + if (typeof height == 'function'){ + height = height(); + } + + instance.rootElement.style.height = height + 'px'; + } + + if (typeof settings.width != 'undefined'){ + var width = settings.width; + + if (typeof width == 'function'){ + width = width(); + } + + instance.rootElement.style.width = width + 'px'; + } + + /* jshint ignore:start */ + if (height){ + instance.rootElement.style.overflow = 'auto'; + } + /* jshint ignore:end */ + + 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')) { + // ignore obj.prototype.type + return; + } + + 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; + }; + + /** + * Returns end of the selection in text input + * @return {Number} + */ + Handsontable.Dom.getSelectionEndPosition = function (el) { + if(el.selectionEnd) { + return el.selectionEnd; + } else if(document.selection) { //IE8 + var r = document.selection.createRange(); + if(r == null) { + return 0; + } + var re = el.createTextRange(); + + return re.text.indexOf(r.text) + r.text.length; + } + }; + + /** + * 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; + }; + + var isIE8 = !(document.createTextNode('test').textContent); + Handsontable.Dom.isIE8 = function () { + return isIE8; + }; + + var isIE9 = !!(document.documentMode); + Handsontable.Dom.isIE9 = function () { + return isIE9; + }; + + var isSafari = (/Safari/.test(navigator.userAgent) && /Apple Computer/.test(navigator.vendor)); + Handsontable.Dom.isSafari = function () { + return isSafari; + }; + + /** + * Sets overlay position depending on it's type and used browser + */ + Handsontable.Dom.setOverlayPosition = function (overlayElem, left, top) { + if (isIE8 || isIE9) { + overlayElem.style.top = top; + overlayElem.style.left = left; + } else if (isSafari) { + overlayElem.style['-webkit-transform'] = 'translate3d(' + left + ',' + top + ',0)'; + } else { + overlayElem.style['transform'] = 'translate3d(' + left + ',' + top + ',0)'; + } + }; + + Handsontable.Dom.getCssTransform = function (elem) { + var transform; + + /* jshint ignore:start */ + if(elem.style['transform'] && (transform = elem.style['transform']) != "") { + return ['transform', transform]; + } else if (elem.style['-webkit-transform'] && (transform = elem.style['-webkit-transform']) != "") { + return ['-webkit-transform', transform]; + } else { + return -1; + } + /* jshint ignore:end */ + }; + + Handsontable.Dom.resetCssTransform = function (elem) { + /* jshint ignore:start */ + if(elem['transform'] && elem['transform'] != "") { + elem['transform'] = ""; + } else if(elem['-webkit-transform'] && elem['-webkit-transform'] != "") { + elem['-webkit-transform'] = ""; + } + /* jshint ignore:end */ + }; + +})(); + + +if(!window.Handsontable){ + var Handsontable = {}; +} + +Handsontable.countEventManagerListeners = 0; //used to debug memory leaks + +Handsontable.eventManager = function (instance) { + var + addEvent, + removeEvent, + clearEvents, + fireEvent; + + if (!instance) { + throw new Error ('instance not defined'); + } + if (!instance.eventListeners) { + instance.eventListeners = []; + } + + /** + * Add Event + * + * @param {Element} element + * @param {String} event + * @param {Function} callback + * @returns {Function} Returns function which you can easily call to remove that event + */ + addEvent = function (element, event, callback) { + var callbackProxy; + + callbackProxy = function (event) { + if (event.target == void 0 && event.srcElement != void 0) { + if (event.definePoperty) { + event.definePoperty('target', { + value: event.srcElement + }); + } else { + event.target = event.srcElement; + } + } + + if (event.preventDefault == void 0) { + if (event.definePoperty) { + event.definePoperty('preventDefault', { + value: function() { + this.returnValue = false; + } + }); + } else { + event.preventDefault = function () { + this.returnValue = false; + }; + } + } + callback.call(this, event); + }; + + instance.eventListeners.push({ + element: element, + event: event, + callback: callback, + callbackProxy: callbackProxy + }); + + if (window.addEventListener) { + element.addEventListener(event, callbackProxy, false); + } else { + element.attachEvent('on' + event, callbackProxy); + } + Handsontable.countEventManagerListeners ++; + + return function _removeEvent() { + removeEvent(element, event, callback); + }; + }; + + /** + * Remove event + * + * @param {Element} element + * @param {String} event + * @param {Function} callback + */ + removeEvent = function (element, event, callback) { + var len = instance.eventListeners.length, + tmpEvent; + + while (len--) { + tmpEvent = instance.eventListeners[len]; + + if (tmpEvent.event == event && tmpEvent.element == element) { + if (callback && callback != tmpEvent.callback) { + continue; + } + instance.eventListeners.splice(len, 1); + + if (tmpEvent.element.removeEventListener) { + tmpEvent.element.removeEventListener(tmpEvent.event, tmpEvent.callbackProxy, false); + } else { + tmpEvent.element.detachEvent('on' + tmpEvent.event, tmpEvent.callbackProxy); + } + Handsontable.countEventManagerListeners --; + } + } + }; + + /** + * Clear all events + */ + clearEvents = function () { + var len = instance.eventListeners.length, + event; + + while (len--) { + event = instance.eventListeners[len]; + + if (event) { + removeEvent(event.element, event.event, event.callback); + } + } + }; + + /** + * Trigger event + * + * @param {Element} element + * @param {String} type + */ + fireEvent = function (element, type) { + var options, event; + + options = { + bubbles: true, + cancelable: (type !== "mousemove"), + view: window, + detail: 0, + screenX: 0, + screenY: 0, + clientX: 1, + clientY: 1, + ctrlKey: false, + altKey: false, + shiftKey: false, + metaKey: false, + button: 0, + relatedTarget: undefined + }; + + if (document.createEvent) { + event = document.createEvent("MouseEvents"); + event.initMouseEvent(type, options.bubbles, options.cancelable, + options.view, options.detail, + options.screenX, options.screenY, options.clientX, options.clientY, + options.ctrlKey, options.altKey, options.shiftKey, options.metaKey, + options.button, options.relatedTarget || document.body.parentNode); + + } else { + event = document.createEventObject(); + } + + if (element.dispatchEvent) { + element.dispatchEvent(event); + } else { + element.fireEvent('on' + type, event); + } + }; + + return { + addEventListener: addEvent, + removeEventListener: removeEvent, + clear: clearEvents, + fireEvent: fireEvent + }; +}; + +/** + * Handsontable TableView constructor + * @param {Object} instance + */ +Handsontable.TableView = function (instance) { + var that = this; + + this.eventManager = Handsontable.eventManager(instance); + this.instance = instance; + this.settings = instance.getSettings(); + + + var originalStyle = instance.rootElement.getAttribute('style'); + if(originalStyle) { + instance.rootElement.setAttribute('data-originalstyle', originalStyle); //needed to retrieve original style in jsFiddle link generator in HT examples. may be removed in future versions + } + + Handsontable.Dom.addClass(instance.rootElement,'handsontable'); +// 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.insertBefore(table, instance.container.firstChild); + + this.eventManager.addEventListener(instance.rootElement,'mousedown', 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. + } + }); + + this.eventManager.addEventListener(document.documentElement, 'keyup',function (event) { + if (instance.selection.isInProgress() && !event.shiftKey) { + instance.selection.finish(); + } + }); + + var isMouseDown; + this.isMouseDown = function () { + return isMouseDown; + }; + + this.eventManager.addEventListener(document.documentElement, 'mouseup', 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(); + } + }); + + this.eventManager.addEventListener(document.documentElement, 'mousedown',function (event) { + var next = event.target; + + if (isMouseDown) { + return; //it must have been started in a cell + } + + 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) { + 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(); + } + }); + + + + this.eventManager.addEventListener(table, 'selectstart', function (event) { + if (that.settings.fragmentSelection) { + return; + } + + //https://github.com/handsontable/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 selections = [ + new WalkontableSelection({ + className: 'current', + border: { + width: 2, + color: '#5292F7', + //style: 'solid', //not used + cornerVisible: function () { + return that.settings.fillHandle && !that.isCellEdited() && !instance.selection.isMultiple(); + }, + multipleSelectionHandlesVisible: function () { + return !that.isCellEdited() && !instance.selection.isMultiple(); + } + } + }), + new WalkontableSelection({ + className: 'area', + border: { + width: 1, + color: '#89AFF9', + //style: 'solid', // not used + cornerVisible: function () { + return that.settings.fillHandle && !that.isCellEdited() && instance.selection.isMultiple(); + }, + multipleSelectionHandlesVisible: function () { + return !that.isCellEdited() && instance.selection.isMultiple(); + } + } + }), + new WalkontableSelection({ + className: 'highlight', + highlightRowClassName: that.settings.currentRowClassName, + highlightColumnClassName: that.settings.currentColClassName + }), + new WalkontableSelection({ + className: 'fill', + border: { + width: 1, + color: 'red' + //style: 'solid' // not used + } + }) + ]; + selections.current = selections[0]; + selections.area = selections[1]; + selections.highlight = selections[2]; + selections.fill = selections[3]; + + var walkontableConfig = { + debug: function () { + return that.settings.debug; + }, + table: table, + stretchH: this.settings.stretchH, + data: instance.getDataAtCell, + totalRows: instance.countRows, + totalColumns: instance.countCols, + fixedColumnsLeft: function () { + return that.settings.fixedColumnsLeft; + }, + fixedRowsTop: function () { + return that.settings.fixedRowsTop; + }, + renderAllRows: that.settings.renderAllRows, + rowHeaders: function () { + var arr = []; + if(instance.hasRowHeaders()) { + arr.push(function (index, TH) { + that.appendRowHeader(index, TH); + }); + } + Handsontable.hooks.run(instance, 'afterGetRowHeaderRenderers', arr); + return arr; + }, + columnHeaders: function () { + + var arr = []; + if(instance.hasColHeaders()) { + arr.push(function (index, TH) { + that.appendColHeader(index, TH); + }); + } + Handsontable.hooks.run(instance, 'afterGetColumnHeaderRenderers', arr); + return arr; + }, + 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: selections, + hideBorderOnMouseDownOver: function () { + return that.settings.fragmentSelection; + }, + onCellMouseDown: function (event, coords, TD, wt) { + instance.listen(); + that.activeWt = wt; + + isMouseDown = true; + + Handsontable.hooks.run(instance, 'beforeOnCellMouseDown', event, coords, TD); + + Handsontable.Dom.enableImmediatePropagation(event); + + if (!event.isImmediatePropagationStopped()) { + + 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); + instance.selection.setSelectedHeaders(false, true); + } + if (coords.col < 0) { + instance.selectCell(coords.row, 0, coords.row, instance.countCols() - 1); + instance.selection.setSelectedHeaders(true, false); + } + } + 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); + } + } else { + if (isMouseDown) { + // multi select columns + if (coords.row < 0) { + instance.selection.setRangeEnd(new WalkontableCellCoords(instance.countRows() - 1, coords.col)); + instance.selection.setSelectedHeaders(false, true); + } + + // multi select rows + if (coords.col < 0) { + instance.selection.setRangeEnd(new WalkontableCellCoords(coords.row, instance.countCols() - 1)); + instance.selection.setSelectedHeaders(true, false); + } + } + } + + 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'); + }, + onBeforeDrawBorders: function (corners, borderClassName) { + instance.runHooks('beforeDrawBorders', corners, borderClassName); + }, + onBeforeTouchScroll: function () { + instance.runHooks('beforeTouchScroll'); + }, + onAfterMomentumScroll: function () { + instance.runHooks('afterMomentumScroll'); + }, + viewportRowCalculatorOverride: function (calc) { + if (that.settings.viewportRowRenderingOffset) { + calc.startRow = Math.max(calc.startRow - that.settings.viewportRowRenderingOffset, 0); + calc.endRow = Math.min(calc.endRow + that.settings.viewportRowRenderingOffset, instance.countRows() - 1); + } + instance.runHooks('afterViewportRowCalculatorOverride', calc); + }, + viewportColumnCalculatorOverride: function (calc) { + if (that.settings.viewportColumnRenderingOffset) { + calc.startColumn = Math.max(calc.startColumn - that.settings.viewportColumnRenderingOffset, 0); + calc.endColumn = Math.min(calc.endColumn + that.settings.viewportColumnRenderingOffset, instance.countCols() - 1); + } + instance.runHooks('afterViewportColumnCalculatorOverride', calc); + } + }; + + Handsontable.hooks.run(instance, 'beforeInitWalkontable', walkontableConfig); + + this.wt = new Walkontable(walkontableConfig); + this.activeWt = this.wt; + + this.eventManager.addEventListener(that.wt.wtTable.spreader, 'mousedown', 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 + Handsontable.helper.stopPropagation(event); + //event.stopPropagation(); + } + }); + + this.eventManager.addEventListener(that.wt.wtTable.spreader, 'contextmenu', 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 + Handsontable.helper.stopPropagation(event); + //event.stopPropagation(); + } + }); + + + this.eventManager.addEventListener(document.documentElement, 'click', 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.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 + * @param {Boolean} topmost + */ +Handsontable.TableView.prototype.getCellAtCoords = function (coords, topmost) { + var td = this.wt.getCell(coords, topmost); + //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, String.fromCharCode(160)); // workaround for https://github.com/handsontable/handsontable/issues/1946 + } + + 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'; + + if (col > -1) { + Handsontable.Dom.fastInnerHTML(SPAN, this.instance.getColHeader(col)); + } else { + Handsontable.Dom.fastInnerText(SPAN, String.fromCharCode(160)); // workaround for https://github.com/handsontable/handsontable/issues/1946 + } + 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} leftOffset + * @return {Number} + */ +Handsontable.TableView.prototype.maximumVisibleElementWidth = function (leftOffset) { + 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) { + 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; +}; + +Handsontable.TableView.prototype.destroy = function () { + this.wt.destroy(); + this.eventManager.clear(); +}; + +/** + * 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 keyCodes = Handsontable.helper.keyCode; + var destroyed = false; + + var eventManager = Handsontable.eventManager(instance); + + var activeEditor; + + var init = function () { + + function onKeyDown(event) { + + if (!instance.isListening()) { + return; + } + + Handsontable.hooks.run(instance, 'beforeKeyDown', event); + + if(destroyed) { + return; + } + + Handsontable.Dom.enableImmediatePropagation(event); + + 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(""); + return; + } + } + + var rangeModifier = event.shiftKey ? selection.setRangeEnd : selection.setRangeStart; + + switch (event.keyCode) { + + case keyCodes.A: + if (ctrlDown) { + selection.selectAll(); //select all cells + + event.preventDefault(); + Handsontable.helper.stopPropagation(event); + //event.stopPropagation(); + } + break; + + case keyCodes.ARROW_UP: + + if (that.isEditorOpened() && !activeEditor.isWaiting()){ + that.closeEditorAndSaveChanges(ctrlDown); + } + + moveSelectionUp(event.shiftKey); + + event.preventDefault(); + Handsontable.helper.stopPropagation(event); + //event.stopPropagation(); //required by HandsontableEditor + break; + + case keyCodes.ARROW_DOWN: + if (that.isEditorOpened() && !activeEditor.isWaiting()){ + that.closeEditorAndSaveChanges(ctrlDown); + } + + moveSelectionDown(event.shiftKey); + + event.preventDefault(); + Handsontable.helper.stopPropagation(event); + //event.stopPropagation(); //required by HandsontableEditor + break; + + case keyCodes.ARROW_RIGHT: + if(that.isEditorOpened() && !activeEditor.isWaiting()){ + that.closeEditorAndSaveChanges(ctrlDown); + } + + moveSelectionRight(event.shiftKey); + + event.preventDefault(); + Handsontable.helper.stopPropagation(event); + //event.stopPropagation(); //required by HandsontableEditor + break; + + case keyCodes.ARROW_LEFT: + if(that.isEditorOpened() && !activeEditor.isWaiting()){ + that.closeEditorAndSaveChanges(ctrlDown); + } + + moveSelectionLeft(event.shiftKey); + + event.preventDefault(); + Handsontable.helper.stopPropagation(event); + //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(); + Handsontable.helper.stopPropagation(event); + //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 + Handsontable.helper.stopPropagation(event); + //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 + Handsontable.helper.stopPropagation(event); + //event.stopPropagation(); //required by HandsontableEditor + break; + + case keyCodes.PAGE_UP: + selection.transformStart(-instance.countVisibleRows(), 0); + event.preventDefault(); //don't page up the window + Handsontable.helper.stopPropagation(event); + //event.stopPropagation(); //required by HandsontableEditor + break; + + case keyCodes.PAGE_DOWN: + selection.transformStart(instance.countVisibleRows(), 0); + event.preventDefault(); //don't page down the window + Handsontable.helper.stopPropagation(event); + //event.stopPropagation(); //required by HandsontableEditor + break; + } + + } + } + } + + instance.addHook('afterDocumentKeyDown', function(originalEvent){ + onKeyDown(originalEvent); + }); + + eventManager.addEventListener(document, 'keydown', function (ev){ + instance.runHooks('afterDocumentKeyDown', ev); + }); + + function onDblClick(event, coords, elem) { + if(elem.nodeName == "TD") { //may be TD or TH + 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); + +Handsontable.helper = {}; + +/** + * 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 + ''; + + case 'object': + if (value === null) { + return ''; + } + else { + return value.toString(); + } + break; + case 'undefined': + return ''; + + 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; +}; + +/** + * Creates 2D array of Excel-like values "A1", "A2", ... + * @param rowCount + * @param colCount + * @returns {Array} + */ +Handsontable.helper.createSpreadsheetData = function(rowCount, colCount) { + rowCount = typeof rowCount === 'number' ? rowCount : 100; + colCount = typeof colCount === 'number' ? colCount : 4; + + var rows = [] + , i + , j; + + for (i = 0; i < rowCount; i++) { + var row = []; + for (j = 0; j < colCount; j++) { + row.push(Handsontable.helper.spreadsheetColumnLabel(j) + (i + 1)); + } + rows.push(row); + } + return rows; +}; + +Handsontable.helper.createSpreadsheetObjectData = function(rowCount, colCount) { + rowCount = typeof rowCount === 'number' ? rowCount : 100; + colCount = typeof colCount === 'number' ? colCount : 4; + + var rows = [] + , i + , j; + + for (i = 0; i < rowCount; i++) { + var row = {}; + for (j = 0; j < colCount; j++) { + row['prop' + j] = Handsontable.helper.spreadsheetColumnLabel(j) + (i + 1); + } + rows.push(row); + } + return rows; +}; + +/** + * 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; +}; + +/** + * 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/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]; + } + } +}; + +/** + * Perform deep 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.deepExtend = function (target, extension) { + for (var key in extension) { + if (extension.hasOwnProperty(key)) { + if (extension[key] && typeof extension[key] === 'object') { + if (!target[key]) { + if (Array.isArray(extension[key])) { + target[key] = []; + } + else { + target[key] = {}; + } + } + Handsontable.helper.deepExtend(target[key], extension[key]); + } + else { + target[key] = extension[key]; + } + } + } +}; + +/** + * Perform deep clone of an object + * WARNING! Only clones JSON properties. Will cause error when `obj` contains a function, Date, etc + * @param {Object} obj An object that will be cloned + * @return {Object} + */ +Handsontable.helper.deepClone = function (obj) { + if (typeof obj === "object") { + return JSON.parse(JSON.stringify(obj)); + } + else { + return obj; + } +}; + +Handsontable.helper.getPrototypeOf = function (obj) { + var prototype; + + /* jshint ignore:start */ + 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 + + } + /* jshint ignore:end */ + + 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, + INSERT: 45, + 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]'; +}; + +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.isMobileBrowser = function (userAgent) { + if(!userAgent) { + userAgent = navigator.userAgent; + } + return (/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(userAgent)); + + // Logic for checking the specific mobile browser + // + /* var type = type != void 0 ? type.toLowerCase() : '' + , result; + switch(type) { + case '': + result = (/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)); + return result; + break; + case 'ipad': + return navigator.userAgent.indexOf('iPad') > -1; + break; + case 'android': + return navigator.userAgent.indexOf('Android') > -1; + break; + case 'windows': + return navigator.userAgent.indexOf('IEMobile') > -1; + break; + default: + throw new Error('Invalid isMobileBrowser argument'); + break; + } */ +}; + +Handsontable.helper.isTouchSupported = function () { + return ('ontouchstart' in window); +}; + +Handsontable.helper.stopPropagation = function (event) { + // ie8 + //http://msdn.microsoft.com/en-us/library/ie/ff975462(v=vs.85).aspx + if (typeof (event.stopPropagation) === 'function') { + event.stopPropagation(); + } + else { + event.cancelBubble = true; + } +}; + +Handsontable.helper.pageX = function (event) { + if (event.pageX) { + return event.pageX; + } + + var scrollLeft = Handsontable.Dom.getWindowScrollLeft(); + var cursorX = event.clientX + scrollLeft; + + return cursorX; +}; + +Handsontable.helper.pageY = function (event) { + if (event.pageY) { + return event.pageY; + } + + var scrollTop = Handsontable.Dom.getWindowScrollTop(); + var cursorY = event.clientY + scrollTop; + + return cursorY; +}; + +(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 (!Array.isArray(obj)){ + schema = {}; + for (var i in obj) { + if (obj.hasOwnProperty(i)) { + if (typeof obj[i] === "object" && !Array.isArray(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 (typeof schema === "object" && !Array.isArray(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 () { + var i, ilen, schema = this.getSchema(); + if (typeof schema === "undefined") { + throw new Error("trying to create `columns` definition but you didnt' provide `schema` nor `data`"); + } + 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.run(this.instance, 'modifyCol', col); + + if (this.colToPropCache && typeof this.colToPropCache[col] !== 'undefined') { + return this.colToPropCache[col]; + } + + 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.run(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 = {}; + Handsontable.helper.deepExtend(row, 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.run(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.run(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.run(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.run(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++) { + + if (typeof out[sliced[i]] === 'undefined'){ + out[sliced[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.run(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 properties (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/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(String.fromCharCode(9660))); // workaround for https://github.com/handsontable/handsontable/issues/1946 +//this is faster than innerHTML. See: https://github.com/handsontable/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(String.fromCharCode(160))); // workaround for https://github.com/handsontable/handsontable/issues/1946 + //this is faster than innerHTML. See: https://github.com/handsontable/handsontable/wiki/JavaScript-&-DOM-performance-tips + } + + + + if (!instance.acArrowListener) { + var eventManager = Handsontable.eventManager(instance); + + //not very elegant but easy and fast + instance.acArrowListener = function (event) { + if (Handsontable.Dom.hasClass(event.target,'htAutocompleteArrow')) { + instance.view.wt.getSetting('onCellDblClick', null, new WalkontableCellCoords(row, col), TD); + } + }; + + eventManager.addEventListener(instance.rootElement,'mousedown',instance.acArrowListener); + + //We need to unbind the listener after the table has been destroyed + instance.addHookOnce('afterDestroy', function () { + eventManager.clear(); + }); + + } + }; + + 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) { + + var eventManager = Handsontable.eventManager(instance); + + 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/handsontable/wiki/JavaScript-&-DOM-performance-tips + } + + if (cellProperties.readOnly) { + eventManager.addEventListener(INPUT,'click',function (event) { + event.preventDefault(); + }); + } + else { + eventManager.addEventListener(INPUT,'mousedown',function (event) { + Handsontable.helper.stopPropagation(event); + //event.stopPropagation(); //otherwise can confuse cell mousedown handler + }); + + eventManager.addEventListener(INPUT,'mouseup',function (event) { + Handsontable.helper.stopPropagation(event); + //event.stopPropagation(); //otherwise can confuse cell dblclick handler + }); + + eventManager.addEventListener(INPUT,'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){ + + Handsontable.Dom.enableImmediatePropagation(event); + + if(event.keyCode == Handsontable.helper.keyCode.SPACE || event.keyCode == Handsontable.helper.keyCode.ENTER){ + + 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; + eventManager.fireEvent(checkbox[i], '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 properties (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(Handsontable){ + + '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); + + }; + + Handsontable.PasswordRenderer = PasswordRenderer; + Handsontable.renderers.PasswordRenderer = PasswordRenderer; + Handsontable.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() + , tmp; + + if(sel[0] > sel[2]) { + tmp = sel[0]; + sel[0] = sel[2]; + sel[2] = tmp; + } + if(sel[1] > sel[3]) { + tmp = sel[1]; + sel[1] = sel[3]; + sel[3] = tmp; + } + + 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) { + var _this = this; + + 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) { + this.instance._registerTimeout(setTimeout(function () { + _this._fireCallbacks(true); + }, 0)); + + return; + } + + if (this.state == Handsontable.EditorState.EDITING) { + if (restoreOriginalValue) { + this.cancelChanges(); + this.instance.view.render(); + + 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)) { + this.instance.addHookOnce('postAfterValidate', function (result) { + _this.state = Handsontable.EditorState.FINISHED; + _this.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; + } + // validator was defined and failed + if (result === false && this.cellProperties.allowInvalid !== true) { + 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(){ + var that = this; + this.createElements(); + this.eventManager = new Handsontable.eventManager(this); + this.bindEvents(); + this.autoResize = autoResize(); + + this.instance.addHook('afterDestroy', function () { + that.destroy(); + }); + }; + + 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) + + Handsontable.Dom.enableImmediatePropagation(event); + + //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; + } + + that.autoResize.resize(String.fromCharCode(event.keyCode)); + }; + + + + TextEditor.prototype.open = function(){ + this.refreshDimensions(); //need it instantly, to prevent https://github.com/handsontable/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'); + + 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.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); + 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(); + var cssTransformOffset; + + // TODO: Refactor this to the new instance.getCell method (from #ply-59), after 0.12.1 is released + switch(editorSection) { + case 'top': + cssTransformOffset = Handsontable.Dom.getCssTransform(this.instance.view.wt.wtScrollbars.vertical.clone.wtTable.holder.parentNode); + break; + case 'left': + cssTransformOffset = Handsontable.Dom.getCssTransform(this.instance.view.wt.wtScrollbars.horizontal.clone.wtTable.holder.parentNode); + break; + case 'corner': + cssTransformOffset = Handsontable.Dom.getCssTransform(this.instance.view.wt.wtScrollbars.corner.clone.wtTable.holder.parentNode); + break; + } + + if (editTop < 0) { + editTop = 0; + } + if (editLeft < 0) { + editLeft = 0; + } + if (rowHeadersCount > 0 && parseInt(this.TD.style.borderTopWidth, 10) > 0) { + editTop += 1; + } + if (colHeadersCount > 0 && parseInt(this.TD.style.borderLeftWidth, 10) > 0) { + editLeft += 1; + } + + + if(cssTransformOffset && cssTransformOffset != -1) { + this.textareaParentStyle[cssTransformOffset[0]] = cssTransformOffset[1]; + } else { + Handsontable.Dom.resetCssTransform(this.textareaParentStyle); + } + + this.textareaParentStyle.top = editTop + 'px'; + this.textareaParentStyle.left = editLeft + 'px'; + + ///end prepare textarea position + + + var cellTopOffset = this.TD.offsetTop - this.instance.view.wt.wtScrollbars.vertical.getScrollPosition(), + cellLeftOffset = this.TD.offsetLeft - this.instance.view.wt.wtScrollbars.horizontal.getScrollPosition(); + + var width = Handsontable.Dom.innerWidth(this.TD) - 8 //$td.width() + , maxWidth = this.instance.view.maximumVisibleElementWidth(cellLeftOffset) - 10 //10 is TEXTAREAs border and padding + , height = Handsontable.Dom.outerHeight(this.TD) - 4 //$td.outerHeight() - 4 + , maxHeight = this.instance.view.maximumVisibleElementHeight(cellTopOffset) - 2; //10 is TEXTAREAs border and padding + + if (parseInt(this.TD.style.borderTopWidth, 10) > 0) { + height -= 1; + } + if (parseInt(this.TD.style.borderLeftWidth, 10) > 0) { + if (rowHeadersCount > 0) { + width -= 1; + } + } + + this.TEXTAREA.style.fontSize = Handsontable.Dom.getComputedStyle(this.TD).fontSize; + this.TEXTAREA.style.fontFamily = Handsontable.Dom.getComputedStyle(this.TD).fontFamily; + + this.autoResize.init(this.TEXTAREA, { + 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) + }, true); + + this.textareaParentStyle.display = 'block'; + }; + + TextEditor.prototype.bindEvents = function () { + var editor = this; + + this.eventManager.addEventListener(this.TEXTAREA, 'cut',function (event){ + Handsontable.helper.stopPropagation(event); + //event.stopPropagation(); + }); + + this.eventManager.addEventListener(this.TEXTAREA, 'paste', function (event){ + Handsontable.helper.stopPropagation(event); + //event.stopPropagation(); + }); + + this.instance.addHook('afterScrollVertically', function () { + editor.refreshDimensions(); + }); + + this.instance.addHook('afterColumnResize', function () { + editor.refreshDimensions(); + editor.focus(); + }); + + this.instance.addHook('afterRowResize', function () { + editor.refreshDimensions(); + editor.focus(); + }); + + this.instance.addHook('afterDestroy', function () { + editor.eventManager.clear(); + }); + }; + + TextEditor.prototype.destroy = function () { + this.eventManager.clear(); + }; + + + Handsontable.editors.TextEditor = TextEditor; + Handsontable.editors.registerEditor('text', Handsontable.editors.TextEditor); + +})(Handsontable); + +(function (Handsontable) { + + var MobileTextEditor = Handsontable.editors.BaseEditor.prototype.extend(); + + var domDimensionsCache = {}; + + var createControls = function () { + this.controls = {}; + + this.controls.leftButton = document.createElement('DIV'); + this.controls.leftButton.className = 'leftButton'; + this.controls.rightButton = document.createElement('DIV'); + this.controls.rightButton.className = 'rightButton'; + this.controls.upButton = document.createElement('DIV'); + this.controls.upButton.className = 'upButton'; + this.controls.downButton = document.createElement('DIV'); + this.controls.downButton.className = 'downButton'; + + for (var button in this.controls) { + if (this.controls.hasOwnProperty(button)) { + this.positionControls.appendChild(this.controls[button]); + } + } + }; + + MobileTextEditor.prototype.valueChanged = function () { + return this.initValue != this.getValue(); + }; + + MobileTextEditor.prototype.init = function () { + var that = this; + this.eventManager = new Handsontable.eventManager(this.instance); + + this.createElements(); + this.bindEvents(); + + this.instance.addHook('afterDestroy', function () { + that.destroy(); + }); + + }; + + MobileTextEditor.prototype.getValue = function () { + return this.TEXTAREA.value; + }; + + MobileTextEditor.prototype.setValue = function (newValue) { + this.initValue = newValue; + + this.TEXTAREA.value = newValue; + }; + + MobileTextEditor.prototype.createElements = function () { + this.editorContainer = document.createElement('DIV'); + this.editorContainer.className = "htMobileEditorContainer"; + + this.cellPointer = document.createElement('DIV'); + this.cellPointer.className = "cellPointer"; + + this.moveHandle = document.createElement('DIV'); + this.moveHandle.className = "moveHandle"; + + this.inputPane = document.createElement('DIV'); + this.inputPane.className = "inputs"; + + this.positionControls = document.createElement('DIV'); + this.positionControls.className = "positionControls"; + + this.TEXTAREA = document.createElement('TEXTAREA'); + Handsontable.Dom.addClass(this.TEXTAREA, 'handsontableInput'); + + this.inputPane.appendChild(this.TEXTAREA); + + this.editorContainer.appendChild(this.cellPointer); + this.editorContainer.appendChild(this.moveHandle); + this.editorContainer.appendChild(this.inputPane); + this.editorContainer.appendChild(this.positionControls); + + createControls.call(this); + + document.body.appendChild(this.editorContainer); + }; + + MobileTextEditor.prototype.onBeforeKeyDown = function (event) { + var instance = this; + var that = instance.getActiveEditor(); + + Handsontable.Dom.enableImmediatePropagation(event); + + if (event.target !== that.TEXTAREA || event.isImmediatePropagationStopped()){ + return; + } + + var keyCodes = Handsontable.helper.keyCode; + + switch(event.keyCode) { + case keyCodes.ENTER: + that.close(); + event.preventDefault(); //don't add newline to field + break; + case keyCodes.BACKSPACE: + event.stopImmediatePropagation(); //backspace, delete, home, end should only work locally when cell is edited (not in table context) + break; + } + }; + + MobileTextEditor.prototype.open = function () { + this.instance.addHook('beforeKeyDown', this.onBeforeKeyDown); + + Handsontable.Dom.addClass(this.editorContainer, 'active'); + //this.updateEditorDimensions(); + //this.scrollToView(); + Handsontable.Dom.removeClass(this.cellPointer, 'hidden'); + + this.updateEditorPosition(); + }; + + MobileTextEditor.prototype.focus = function(){ + this.TEXTAREA.focus(); + Handsontable.Dom.setCaretPosition(this.TEXTAREA, this.TEXTAREA.value.length); + }; + + MobileTextEditor.prototype.close = function () { + this.TEXTAREA.blur(); + this.instance.removeHook('beforeKeyDown', this.onBeforeKeyDown); + + Handsontable.Dom.removeClass(this.editorContainer, 'active'); + }; + + MobileTextEditor.prototype.scrollToView = function () { + var coords = this.instance.getSelectedRange().highlight; + this.instance.view.scrollViewport(coords); + }; + + MobileTextEditor.prototype.hideCellPointer = function () { + if(!Handsontable.Dom.hasClass(this.cellPointer, 'hidden')) { + Handsontable.Dom.addClass(this.cellPointer, 'hidden'); + } + }; + + MobileTextEditor.prototype.updateEditorPosition = function (x, y) { + if(x && y) { + x = parseInt(x, 10); + y = parseInt(y, 10); + + this.editorContainer.style.top = y + "px"; + this.editorContainer.style.left = x + "px"; + + } else { + var selection = this.instance.getSelected() + , selectedCell = this.instance.getCell(selection[0],selection[1]); + + //cache sizes + if(!domDimensionsCache.cellPointer) { + domDimensionsCache.cellPointer = { + height: Handsontable.Dom.outerHeight(this.cellPointer), + width: Handsontable.Dom.outerWidth(this.cellPointer) + }; + } + if(!domDimensionsCache.editorContainer) { + domDimensionsCache.editorContainer = { + width: Handsontable.Dom.outerWidth(this.editorContainer) + }; + } + + if(selectedCell !== undefined) { + var scrollLeft = this.instance.view.wt.wtScrollbars.horizontal.scrollHandler == window ? + 0 : Handsontable.Dom.getScrollLeft(this.instance.view.wt.wtScrollbars.horizontal.scrollHandler); + var scrollTop = this.instance.view.wt.wtScrollbars.vertical.scrollHandler == window ? + 0 : Handsontable.Dom.getScrollTop(this.instance.view.wt.wtScrollbars.vertical.scrollHandler); + + var selectedCellOffset = Handsontable.Dom.offset(selectedCell) + , selectedCellWidth = Handsontable.Dom.outerWidth(selectedCell) + , currentScrollPosition = { + x: scrollLeft, + y: scrollTop + }; + + this.editorContainer.style.top = parseInt(selectedCellOffset.top + Handsontable.Dom.outerHeight(selectedCell) - + currentScrollPosition.y + domDimensionsCache.cellPointer.height, 10) + "px"; + this.editorContainer.style.left = parseInt((window.innerWidth / 2) - + (domDimensionsCache.editorContainer.width / 2) ,10) + "px"; + + if(selectedCellOffset.left + selectedCellWidth / 2 > parseInt(this.editorContainer.style.left,10) + domDimensionsCache.editorContainer.width) { + this.editorContainer.style.left = window.innerWidth - domDimensionsCache.editorContainer.width + "px"; + } else if(selectedCellOffset.left + selectedCellWidth / 2 < parseInt(this.editorContainer.style.left,10) + 20) { + this.editorContainer.style.left = 0 + "px"; + } + + this.cellPointer.style.left = parseInt(selectedCellOffset.left - (domDimensionsCache.cellPointer.width / 2) - + Handsontable.Dom.offset(this.editorContainer).left + (selectedCellWidth / 2) - currentScrollPosition.x ,10) + "px"; + } + } + }; + + + // For the optional dont-affect-editor-by-zooming feature: + + //MobileTextEditor.prototype.updateEditorDimensions = function () { + // if(!this.beginningWindowWidth) { + // this.beginningWindowWidth = window.innerWidth; + // this.beginningEditorWidth = Handsontable.Dom.outerWidth(this.editorContainer); + // this.scaleRatio = this.beginningEditorWidth / this.beginningWindowWidth; + // + // this.editorContainer.style.width = this.beginningEditorWidth + "px"; + // return; + // } + // + // var currentScaleRatio = this.beginningEditorWidth / window.innerWidth; + // //if(currentScaleRatio > this.scaleRatio + 0.2 || currentScaleRatio < this.scaleRatio - 0.2) { + // if(currentScaleRatio != this.scaleRatio) { + // this.editorContainer.style["zoom"] = (1 - ((currentScaleRatio * this.scaleRatio) - this.scaleRatio)) * 100 + "%"; + // } + // + //}; + + MobileTextEditor.prototype.updateEditorData = function () { + var selected = this.instance.getSelected() + , selectedValue = this.instance.getDataAtCell(selected[0], selected[1]); + + this.row = selected[0]; + this.col = selected[1]; + this.setValue(selectedValue); + this.updateEditorPosition(); + }; + + MobileTextEditor.prototype.prepareAndSave = function () { + + if(!this.valueChanged()) { + return true; + } + + var val = [ + [String.prototype.trim.call(this.getValue())] + ]; + + this.saveValue(val); + }; + + MobileTextEditor.prototype.bindEvents = function () { + var that = this; + + this.eventManager.addEventListener(this.controls.leftButton, "touchend", function (event) { + that.prepareAndSave(); + that.instance.selection.transformStart(0, -1, null, true); + that.updateEditorData(); + event.preventDefault(); + }); + this.eventManager.addEventListener(this.controls.rightButton, "touchend", function (event) { + that.prepareAndSave(); + that.instance.selection.transformStart(0, 1, null, true); + that.updateEditorData(); + event.preventDefault(); + }); + this.eventManager.addEventListener(this.controls.upButton, "touchend", function (event) { + that.prepareAndSave(); + that.instance.selection.transformStart(-1, 0, null, true); + that.updateEditorData(); + event.preventDefault(); + }); + this.eventManager.addEventListener(this.controls.downButton, "touchend", function (event) { + that.prepareAndSave(); + that.instance.selection.transformStart(1, 0, null, true); + that.updateEditorData(); + event.preventDefault(); + }); + + this.eventManager.addEventListener(this.moveHandle, "touchstart", function (event) { + if (event.touches.length == 1) { + var touch = event.touches[0] + , onTouchPosition = { + x: that.editorContainer.offsetLeft, + y: that.editorContainer.offsetTop + } + , onTouchOffset = { + x: touch.pageX - onTouchPosition.x, + y: touch.pageY - onTouchPosition.y + }; + + that.eventManager.addEventListener(this, "touchmove", function (event) { + var touch = event.touches[0]; + that.updateEditorPosition(touch.pageX - onTouchOffset.x, touch.pageY - onTouchOffset.y); + that.hideCellPointer(); + event.preventDefault(); + }); + + } + }); + + this.eventManager.addEventListener(document.body, "touchend", function (event) { + if(!Handsontable.Dom.isChildOf(event.target, that.editorContainer) && !Handsontable.Dom.isChildOf(event.target, that.instance.rootElement)) { + that.close(); + } + }); + + this.eventManager.addEventListener(this.instance.view.wt.wtScrollbars.horizontal.scrollHandler, "scroll", function (event) { + if(that.instance.view.wt.wtScrollbars.horizontal.scrollHandler != window) { + that.hideCellPointer(); + } + }); + + this.eventManager.addEventListener(this.instance.view.wt.wtScrollbars.vertical.scrollHandler, "scroll", function (event) { + if(that.instance.view.wt.wtScrollbars.vertical.scrollHandler != window) { + that.hideCellPointer(); + } + }); + + }; + + MobileTextEditor.prototype.destroy = function () { + this.eventManager.clear(); + + this.editorContainer.parentNode.removeChild(this.editorContainer); + }; + + Handsontable.editors.MobileTextEditor = MobileTextEditor; + Handsontable.editors.registerEditor('mobile', Handsontable.editors.MobileTextEditor); + + + +})(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.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(); + + var $; + + DateEditor.prototype.init = function () { + if (typeof jQuery != 'undefined') { + $ = jQuery; + } else { + throw new Error("You need to include jQuery to your project in order to use the jQuery UI Datepicker."); + } + + 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); + + var eventManager = Handsontable.eventManager(this); + + /** + * Prevent recognizing clicking on jQuery Datepicker as clicking outside of table + */ + eventManager.addEventListener(this.datePicker, 'mousedown', function (event) { + Handsontable.helper.stopPropagation(event); + //event.stopPropagation(); + }); + + this.hideDatepicker(); + }; + + DateEditor.prototype.destroyElements = function () { + this.$datePicker.datepicker('destroy'); + this.$datePicker.remove(); + //var eventManager = Handsontable.eventManager(this); + //eventManager.removeEventListener(this.datePicker, 'mousedown'); + }; + + 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 offset = this.TD.getBoundingClientRect(), + DatepickerSettings, + datepickerSettings; + + this.datePickerStyle.top = (window.pageYOffset + offset.top + Handsontable.Dom.outerHeight(this.TD)) + 'px'; + this.datePickerStyle.left = (window.pageXOffset + offset.left) + 'px'; + + DatepickerSettings = function () {}; + DatepickerSettings.prototype = this.cellProperties; + datepickerSettings = new DatepickerSettings(); + datepickerSettings.defaultDate = this.originalValue || void 0; + this.$datePicker.datepicker('option', datepickerSettings); + + 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.htEditor = new Handsontable(DIV); + + this.assignHooks(); + }; + + 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(); + } + }; + + if (this.cellProperties.handsontable) { + Handsontable.helper.extend(options, cellProperties.handsontable); + } + if (this.htEditor) { + this.htEditor.destroy(); + } + + this.htEditor = new Handsontable(this.htContainer, options); + + //this.$htContainer.handsontable('destroy'); + //this.$htContainer.handsontable(options); + }; + + var onBeforeKeyDown = function (event) { + + if (event != null && event.isImmediatePropagationEnabled == null) { + event.stopImmediatePropagation = function () { + this.isImmediatePropagationEnabled = false; + this.cancelBubble = true; + }; + event.isImmediatePropagationEnabled = true; + event.isImmediatePropagationStopped = function () { + return !this.isImmediatePropagationEnabled; + }; + } + + if (event.isImmediatePropagationStopped()) { + return; + } + + var editor = this.getActiveEditor(); + + var innerHOT = editor.htEditor.getInstance(); //Handsontable.tmpHandsontable(editor.htContainer, 'getInstance'); + + var rowToSelect; + + if (event.keyCode == Handsontable.helper.keyCode.ARROW_DOWN) { + if (!innerHOT.getSelected()) { + rowToSelect = 0; + } + else { + var selectedRow = innerHOT.getSelected()[0]; + var lastRow = innerHOT.countRows() - 1; + rowToSelect = Math.min(lastRow, selectedRow + 1); + } + } + else if (event.keyCode == Handsontable.helper.keyCode.ARROW_UP) { + if (innerHOT.getSelected()) { + var selectedRow = innerHOT.getSelected()[0]; + rowToSelect = selectedRow - 1; + } + } + + if (rowToSelect !== void 0) { + if (rowToSelect < 0) { + innerHOT.deselectCell(); + } + else { + innerHOT.selectCell(rowToSelect, 0); + } + + event.preventDefault(); + event.stopImmediatePropagation(); + + editor.instance.listen(); + editor.TEXTAREA.focus(); + } + }; + + HandsontableEditor.prototype.open = function () { + + this.instance.addHook('beforeKeyDown', onBeforeKeyDown); + + Handsontable.editors.TextEditor.prototype.open.apply(this, arguments); + + //this.$htContainer.handsontable('render'); + + //Handsontable.tmpHandsontable(this.htContainer, 'render'); + this.htEditor.render(); + + if (this.cellProperties.strict) { + this.htEditor.selectCell(0,0); + this.TEXTAREA.style.visibility = 'hidden'; + } else { + this.htEditor.deselectCell(); + this.TEXTAREA.style.visibility = 'visible'; + } + + Handsontable.Dom.setCaretPosition(this.TEXTAREA, 0, this.TEXTAREA.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.htEditor.isListening()) { //if focus is still in the HOT editor + + //if (Handsontable.tmpHandsontable(this.htContainer,'isListening')) { //if focus is still in the HOT editor + //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.htEditor.getSelected()){ + //if (Handsontable.tmpHandsontable(this.htContainer,'getSelected')) { + //if (this.$htContainer.handsontable('getSelected')) { + // var value = this.$htContainer.handsontable('getInstance').getValue(); + var value = this.htEditor.getInstance().getValue(); + //var value = Handsontable.tmpHandsontable(this.htContainer,'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); + }; + + HandsontableEditor.prototype.assignHooks = function () { + var that = this; + this.instance.addHook('afterDestroy', function () { + if (that.htEditor) { + that.htEditor.destroy(); + } + }); + + }; + + 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); + + // set choices list initial height, so Walkontable can assign it's scroll handler + var choicesListHot = this.htEditor.getInstance(); + choicesListHot.updateSettings({ + height: 1 + }); + + this.query = null; + this.choices = []; + }; + + AutocompleteEditor.prototype.createElements = function(){ + Handsontable.editors.HandsontableEditor.prototype.createElements.apply(this, arguments); + + var getSystemSpecificPaddingClass = function () { + if(window.navigator.platform.indexOf('Mac') != -1) { + return "htMacScroll"; + } else { + return ""; + } + }; + + Handsontable.Dom.addClass(this.htContainer, 'autocompleteEditor'); + Handsontable.Dom.addClass(this.htContainer, getSystemSpecificPaddingClass()); + + }; + + var skipOne = false; + var onBeforeKeyDown = function (event) { + skipOne = false; + var editor = this.getActiveEditor(); + var keyCodes = Handsontable.helper.keyCode; + + if (Handsontable.helper.isPrintableChar(event.keyCode) || event.keyCode === keyCodes.BACKSPACE || event.keyCode === keyCodes.DELETE || event.keyCode === keyCodes.INSERT) { + var timeOffset = 0; + + // on ctl+c / cmd+c don't update suggestion list + if(event.keyCode === keyCodes.C && (event.ctrlKey || event.metaKey)) { + return; + } + + if(!editor.isOpened()) { + timeOffset += 10; + } + + editor.instance._registerTimeout(setTimeout(function () { + editor.queryChoices(editor.TEXTAREA.value); + skipOne = true; + }, timeOffset)); + } + }; + + AutocompleteEditor.prototype.prepare = function () { + this.instance.addHook('beforeKeyDown', onBeforeKeyDown); + Handsontable.editors.HandsontableEditor.prototype.prepare.apply(this, arguments); + }; + + AutocompleteEditor.prototype.open = function () { + Handsontable.editors.HandsontableEditor.prototype.open.apply(this, arguments); + + this.TEXTAREA.style.visibility = 'visible'; + this.focus(); + + this.htContainer.style.overflow = 'hidden'; // small hack to prevent vertical scrollbar causing a horizontal scrollbar + + var choicesListHot = this.htEditor.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; + + if(value){ + 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 + ''); + } + } + } + }); + + if(skipOne) { + skipOne = false; + } + that.instance._registerTimeout(setTimeout(function () { + that.queryChoices(that.TEXTAREA.value); + that.htContainer.style.overflow = 'auto'; // small hack to prevent vertical scrollbar causing a horizontal scrollbar + }, 0)); + + }; + + AutocompleteEditor.prototype.close = function () { + 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 (Array.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) { + var pos = Handsontable.Dom.getCaretPosition(this.TEXTAREA), + endPos = Handsontable.Dom.getSelectionEndPosition(this.TEXTAREA); + + var orderByRelevance = AutocompleteEditor.sortByRelevance(this.getValue(), choices, this.cellProperties.filteringCaseSensitive); + var highlightIndex; + + /* jshint ignore:start */ + if (this.cellProperties.filter != false) { + var sorted = []; + for(var i = 0, choicesCount = orderByRelevance.length; i < choicesCount; i++) { + sorted.push(choices[orderByRelevance[i]]); + } + highlightIndex = 0; + choices = sorted; + } + else { + highlightIndex = orderByRelevance[0]; + } + /* jshint ignore:end */ + + this.choices = choices; + + this.htEditor.loadData(Handsontable.helper.pivot([choices])); + this.htEditor.updateSettings({height: this.getDropdownHeight()}); + //Handsontable.tmpHandsontable(this.htContainer,'loadData', Handsontable.helper.pivot([choices])); + //Handsontable.tmpHandsontable(this.htContainer,'updateSettings', {height: this.getDropdownHeight()}); + + if (this.cellProperties.strict === true) { + this.highlightBestMatchingChoice(highlightIndex); + } + + this.instance.listen(); + this.TEXTAREA.focus(); + Handsontable.Dom.setCaretPosition(this.TEXTAREA, pos, (pos != endPos ? endPos : void 0)); + }; + + AutocompleteEditor.prototype.finishEditing = function (restoreOriginalValue) { + if (!restoreOriginalValue) { + this.instance.removeHook('beforeKeyDown', onBeforeKeyDown); + } + Handsontable.editors.HandsontableEditor.prototype.finishEditing.apply(this, arguments); + }; + + AutocompleteEditor.prototype.highlightBestMatchingChoice = function (index) { + if (typeof index === "number") { + this.htEditor.selectCell(index, 0); + } else { + this.htEditor.deselectCell(); + } + }; + + /** + * Filters and sorts by relevance + * @param value + * @param choices + * @param caseSensitive + * @returns {Array} array of indexes in original choices array + */ + AutocompleteEditor.sortByRelevance = function(value, choices, caseSensitive) { + + var choicesRelevance = [] + , currentItem + , valueLength = value.length + , valueIndex + , charsLeft + , result = [] + , i + , choicesCount; + + if(valueLength === 0) { + for(i = 0, choicesCount = choices.length; i < choicesCount; i++) { + result.push(i); + } + return result; + } + + for(i = 0, choicesCount = choices.length; i < choicesCount; i++) { + currentItem = choices[i]; + + if(caseSensitive) { + valueIndex = currentItem.indexOf(value); + } else { + valueIndex = currentItem.toLowerCase().indexOf(value.toLowerCase()); + } + + + if(valueIndex == -1) { continue; } + charsLeft = currentItem.length - valueIndex - valueLength; + + choicesRelevance.push({ + baseIndex: i, + index: valueIndex, + charsLeft: charsLeft, + value: currentItem + }); + } + + choicesRelevance.sort(function(a, b) { + + if(b.index === -1) { + return -1; + } + if(a.index === -1) { + return 1; + } + + if(a.index < b.index) { + return -1; + } else if(b.index < a.index) { + return 1; + } else if(a.index === b.index) { + if(a.charsLeft < b.charsLeft) { + return -1; + } else if(a.charsLeft > b.charsLeft) { + return 1; + } else { + return 0; + } + } + }); + + for(i = 0, choicesCount = choicesRelevance.length; i < choicesCount; i++) { + result.push(choicesRelevance[i].baseIndex); + } + + return result; + }; + + AutocompleteEditor.prototype.getDropdownHeight = function(){ + //var firstRowHeight = this.$htContainer.handsontable('getInstance').getRowHeight(0) || 23; + var firstRowHeight = this.htEditor.getInstance().getRowHeight(0) || 23; + //var firstRowHeight = Handsontable.tmpHandsontable(this.htContainer,'getInstance').getRowHeight(0) || 23; + return this.choices.length >= 10 ? 10 * firstRowHeight : this.choices.length * firstRowHeight + 8; + //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; + + 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.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 (Array.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; + } + }; + + // TODO: Refactor this with the use of new getCell() after 0.12.1 + SelectEditor.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'; + } + } + }; + + 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); + var tdOffset = Handsontable.Dom.offset(this.TD); + var editorSection = this.checkEditorSection(); + var cssTransformOffset; + + switch(editorSection) { + case 'top': + cssTransformOffset = Handsontable.Dom.getCssTransform(this.instance.view.wt.wtScrollbars.vertical.clone.wtTable.holder.parentNode); + break; + case 'left': + cssTransformOffset = Handsontable.Dom.getCssTransform(this.instance.view.wt.wtScrollbars.horizontal.clone.wtTable.holder.parentNode); + break; + case 'corner': + cssTransformOffset = Handsontable.Dom.getCssTransform(this.instance.view.wt.wtScrollbars.corner.clone.wtTable.holder.parentNode); + break; + } + + var selectStyle = this.select.style; + + if(cssTransformOffset && cssTransformOffset != -1) { + selectStyle[cssTransformOffset[0]] = cssTransformOffset[1]; + } else { + Handsontable.Dom.resetCssTransform(this.select); + } + + selectStyle.height = height + 'px'; + selectStyle.minWidth = width + 'px'; + selectStyle.top = tdOffset.top - rootOffset.top + 'px'; + selectStyle.left = tdOffset.left - rootOffset.left + 'px'; + selectStyle.margin = '0px'; + selectStyle.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); +(function (Handsontable) { + + 'use strict'; + + var NumericEditor = Handsontable.editors.TextEditor.prototype.extend(); + + NumericEditor.prototype.beginEditing = function (initialValue) { + + var BaseEditor = Handsontable.editors.TextEditor.prototype; + + if (typeof (initialValue) === 'undefined' && this.originalValue) { + + var value = '' + this.originalValue; + + if (typeof this.cellProperties.language !== 'undefined') { + numeral.language(this.cellProperties.language); + } + + var decimalDelimiter = numeral.languageData().delimiters.decimal; + value = value.replace('.', decimalDelimiter); + + BaseEditor.beginEditing.apply(this, [value]); + } else { + BaseEditor.beginEditing.apply(this, arguments); + } + + }; + + Handsontable.editors.NumericEditor = NumericEditor; + Handsontable.editors.registerEditor('numeric', NumericEditor); + +})(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 {*} callback - Callback called with validation result + */ +Handsontable.AutocompleteValidator = function (value, callback) { + if (this.strict && this.source) { + if ( typeof this.source === 'function' ) { + this.source(value, process(value, callback)); + } else { + process(value, callback)(this.source); + } + } else { + callback(true); + } +}; + +/** + * Cell type is just a shortcut for setting bunch of cellProperties (used in getCellMeta) + */ + +Handsontable.mobileBrowser = Handsontable.helper.isMobileBrowser(); // check if viewed on a mobile device + +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.mobileBrowser ? Handsontable.editors.MobileTextEditor : Handsontable.editors.TextEditor, + renderer: Handsontable.renderers.TextRenderer +}; + +Handsontable.NumericCell = { + editor: Handsontable.editors.NumericEditor, + 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 + }, + 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.removeEventListener) { + element.removeEventListener(event, handler, false); + } else { + element.detachEvent('on' + event, handler); + } + }, + resize = function (newChar) { + var width, scrollHeight; + + if (!newChar) { + newChar = ""; + } else if (!/^[a-zA-Z \.,\\\/\|0-9]$/.test(newChar)) { + newChar = "."; + } + + if (text.textContent !== void 0) { + text.textContent = el.value + newChar; + } + else { + text.data = el.value + newChar; //IE8 + } + span.style.fontSize = Handsontable.Dom.getComputedStyle(el).fontSize; + span.style.fontFamily = Handsontable.Dom.getComputedStyle(el).fontFamily; + span.style.whiteSpace = "pre"; + + body.appendChild(span); + width = span.clientWidth + 2; + 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'; + } + scrollHeight = el.scrollHeight ? el.scrollHeight - 1 : 0; + + 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(!span.firstChild) { + span.className = "autoResize"; + span.style.display = 'inline-block'; + span.appendChild(text); + } + }, + init = function (el_, config, doObserve) { + 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.overflowY = 'hidden'; + } + + if(doObserve) { + 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, doObserve) { + init(el_, config, doObserve); + }, + unObserve: function () { + unObserve(el, 'change', resize); + unObserve(el, 'cut', delayedResize); + unObserve(el, 'paste', delayedResize); + unObserve(el, 'drop', delayedResize); + unObserve(el, 'keydown', delayedResize); + }, + resize: resize + }; + +}; + +/** + * 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 = { + /** + * Decode spreadsheet string into array + * + * @param {String} str + * @returns {Array} + */ + 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; + }, + + /** + * Encode array into valid spreadsheet string + * + * @param arr + * @returns {String} + */ + stringify: function (arr) { + var r, rLen, c, cLen, str = '', val; + + for (r = 0, rLen = arr.length; r < rLen; r += 1) { + cLen = arr[r].length; + + for (c = 0; 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)); + +/** + * 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(); +} + +/** + * Initialize CopyPaste class + */ +CopyPasteClass.prototype.init = function () { + var + style, + parent; + + this.copyCallbacks = []; + this.cutCallbacks = []; + this.pasteCallbacks = []; + this._eventManager = Handsontable.eventManager(this); + + // 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'; + this.elTextarea.onpaste = function (event) { + if('WebkitAppearance' in document.documentElement.style) { // chrome and safari + this.value = event.clipboardData.getData("Text"); + + return false; + } + }; + 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; + } + } + this.keyDownRemoveEvent = this._eventManager.addEventListener(document.documentElement, 'keydown', this.onKeyDown.bind(this), false); +}; + +/** + * Call method on every key down event + * + * @param {DOMEvent} event + */ +CopyPasteClass.prototype.onKeyDown = function (event) { + var _this = this, + isCtrlDown = false; + + // mac + if (event.metaKey) { + isCtrlDown = true; + } + // pc + else if (event.ctrlKey && navigator.userAgent.indexOf('Mac') === -1) { + isCtrlDown = true; + } + if (isCtrlDown) { + // this is needed by fragmentSelection in Handsontable. Ignore copypaste.js behavior if fragment of cell text is selected + if (document.activeElement !== this.elTextarea && (this.getSelectionText() !== '' || + ['INPUT', 'SELECT', 'TEXTAREA'].indexOf(document.activeElement.nodeName) !== -1)) { + return; + } + + this.selectNodeText(this.elTextarea); + setTimeout(function () { + _this.selectNodeText(_this.elTextarea); + }, 0); + } + + /* 67 = c + * 86 = v + * 88 = x + */ + if (isCtrlDown && (event.keyCode === 67 || event.keyCode === 86 || event.keyCode === 88)) { + // that.selectNodeText(that.elTextarea); + + // works in all browsers, incl. Opera < 12.12 + if (event.keyCode === 88) { + setTimeout(function () { + _this.triggerCut(event); + }, 0); + } + else if (event.keyCode === 86) { + setTimeout(function () { + _this.triggerPaste(event); + }, 0); + } + } +}; + +//http://jsperf.com/textara-selection +//http://stackoverflow.com/questions/1502385/how-can-i-make-this-code-work-in-ie +/** + * Select all text contains in passed node element + * + * @param {Element} el + */ +CopyPasteClass.prototype.selectNodeText = function (el) { + if (el) { + el.select(); + } +}; + +//http://stackoverflow.com/questions/5379120/get-the-highlighted-selected-text +/** + * Get selection text + * + * @returns {String} + */ +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; +}; + +/** + * Make string copyable + * + * @param {String} str + */ +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); +};*/ + +/** + * Add function callback to onCut event + * + * @param {Function} fn + */ +CopyPasteClass.prototype.onCut = function (fn) { + this.cutCallbacks.push(fn); +}; + +/** + * Add function callback to onPaste event + * + * @param {Function} fn + */ +CopyPasteClass.prototype.onPaste = function (fn) { + this.pasteCallbacks.push(fn); +}; + +/** + * Remove callback from all events + * + * @param {Function} fn + * @returns {Boolean} + */ +CopyPasteClass.prototype.removeCallback = function (fn) { + var i, len; + + for (i = 0, len = this.copyCallbacks.length; i < len; i++) { + if (this.copyCallbacks[i] === fn) { + this.copyCallbacks.splice(i, 1); + + return true; + } + } + for (i = 0, len = this.cutCallbacks.length; i < len; i++) { + if (this.cutCallbacks[i] === fn) { + this.cutCallbacks.splice(i, 1); + + return true; + } + } + for (i = 0, len = this.pasteCallbacks.length; i < len; i++) { + if (this.pasteCallbacks[i] === fn) { + this.pasteCallbacks.splice(i, 1); + + return true; + } + } + + return false; +}; + +/** + * Trigger cut event + * + * @param {DOMEvent} event + */ +CopyPasteClass.prototype.triggerCut = function (event) { + var _this = this; + + if (_this.cutCallbacks) { + setTimeout(function () { + for (var i = 0, len = _this.cutCallbacks.length; i < len; i++) { + _this.cutCallbacks[i](event); + } + }, 50); + } +}; + +/** + * Trigger paste event + * + * @param {DOMEvent} event + * @param {String} str + */ +CopyPasteClass.prototype.triggerPaste = function (event, str) { + var _this = this; + + if (_this.pasteCallbacks) { + setTimeout(function () { + var val = str || _this.elTextarea.value; + + for (var i = 0, len = _this.pasteCallbacks.length; i < len; i++) { + _this.pasteCallbacks[i](val, event); + } + }, 50); + } +}; + +/** + * Destroy instance + */ +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.keyDownRemoveEvent(); + } +}; + +/** + * Check if instance has been destroyed + * + * @returns {Boolean} + */ +CopyPasteClass.prototype.hasBeenDestroyed = function () { + return !this.refCounter; +}; + + + +// 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); + /* jshint ignore:start */ + if (found != '') { + return escapePathComponent(key) + '/' + found; + } + /* jshint ignore:end */ + } + } + } + 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 = []; + /* jshint ignore:start */ + jsonpatch.intervals; + /* jshint ignore:end */ + 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; + /* jshint ignore:start */ + 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++; + } + /* jshint ignore:end */ + + 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: [], + beforeDrawBorders: [], + beforeChange: [], + beforeChangeRender: [], + beforeRemoveCol: [], + beforeRemoveRow: [], + beforeValidate: [], + beforeGetCellMeta: [], + beforeAutofill: [], + beforeKeyDown: [], + beforeOnCellMouseDown: [], + beforeTouchScroll: [], + 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: [], + afterIsMultipleSelectionCheck: [], + afterDocumentKeyDown: [], + afterMomentumScroll: [], + + // 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() { + /* jshint ignore:start */ + this.hooks = Hooks(); + /* jshint ignore:end */ + 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 (Array.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 been added (adding the same hook twice is now silently ignored) + } + } + return this; + }; + + PluginHookClass.prototype.once = function(key, fn, instance){ + + if(Array.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.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.run = 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._runBucket(this.globalBucket, instance, key, p1, p2, p3, p4, p5, p6); + p1 = this._runBucket(this.getBucket(instance), instance, key, p1, p2, p3, p4, p5, p6); + + return p1; + }; + + PluginHookClass.prototype._runBucket = function (bucket, instance, key, p1, p2, p3, p4, p5, p6) { + var handlers = bucket[key], + res, i, len; + + // performance considerations - http://jsperf.com/call-vs-apply-for-a-plugin-architecture + if (handlers) { + for (i = 0, len = handlers.length; i < len; 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); + } + } + } + } + + 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.className + ' htAutoColumnSize'; + tmp.table.className = instance.table.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.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.className + ' hidden'; +// 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; + + var eventManager = Handsontable.eventManager(instance); + eventManager.addEventListener(instance.rootElement, 'click', 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(typeof a[1] == "string") { + a[1] = a[1].toLowerCase(); + } + if(typeof b[1] == "string") { + b[1] = b[1].toLowerCase(); + } + + if (a[1] === b[1]) { + return 0; + } + if (a[1] === null || a[1] === "") { + return 1; + } + if (b[1] === null || b[1] === "") { + 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 && col >= 0) { + 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) { + /* jshint ignore:start */ + 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); + + } + + function align(range, type, alignment) { + /* jshint ignore:start */ + 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); + } + } + } + + this.render(); + + /* jshint ignore:end */ + } + + function ContextMenu(instance, customOptions) { + this.instance = instance; + var contextMenu = this; + contextMenu.menus = []; + contextMenu.htMenus = {}; + contextMenu.triggerRows = []; + + contextMenu.eventManager = Handsontable.eventManager(contextMenu); + + + this.enabled = true; + + this.instance.addHook('afterDestroy', function () { + contextMenu.destroy(); + }); + + this.defaultOptions = { + items: [ + { + key: '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.countRows() - 1, selected[1]], + columnSelected = entireColumnSelection.join(',') == selected.join(','); + + return selected[0] < 0 || this.countRows() >= this.getSettings().maxRows || columnSelected; + } + }, + { + key: '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.countRows() - 1, selected[1]], + columnSelected = entireColumnSelection.join(',') == selected.join(','); + + return this.getSelected()[0] < 0 || this.countRows() >= this.getSettings().maxRows || columnSelected; + } + }, + ContextMenu.SEPARATOR, + { + key: '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.countCols() - 1], + rowSelected = entireRowSelection.join(',') == selected.join(','); + + return this.getSelected()[1] < 0 || this.countCols() >= this.getSettings().maxCols || rowSelected; + } + }, + { + key: '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.countCols() - 1], + rowSelected = entireRowSelection.join(',') == selected.join(','); + + return selected[1] < 0 || this.countCols() >= this.getSettings().maxCols || rowSelected; + } + }, + ContextMenu.SEPARATOR, + { + key: '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.countRows() - 1, selected[1]], + columnSelected = entireColumnSelection.join(',') == selected.join(','); + return (selected[0] < 0 || columnSelected); + } + }, + { + key: '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.countCols() - 1], + rowSelected = entireRowSelection.join(',') == selected.join(','); + return (selected[1] < 0 || rowSelected); + } + }, + ContextMenu.SEPARATOR, + { + key: 'undo', + name: 'Undo', + callback: function () { + this.undo(); + }, + disabled: function () { + return this.undoRedo && !this.undoRedo.isUndoAvailable(); + } + }, + { + key: 'redo', + name: 'Redo', + callback: function () { + this.redo(); + }, + disabled: function () { + return this.undoRedo && !this.undoRedo.isRedoAvailable(); + } + }, + ContextMenu.SEPARATOR, + { + key: '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(); + } + }, + ContextMenu.SEPARATOR, + { + key: 'alignment', + name: 'Alignment', + submenu: { + items: [ + { + 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 + }, + { + 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 + }, + { + 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 + }, + { + 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 + }, + ContextMenu.SEPARATOR, + { + 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 + }, + { + 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 + }, + { + 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.options); + + this.bindMouseEvents(); + + this.markSelected = function (label) { + return "" + String.fromCharCode(10003) + "" + label; // workaround for https://github.com/handsontable/handsontable/issues/1946 + }; + + 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; + }; + + if(!this.instance.getSettings().allowInsertRow) { + var rowAboveIndex = findIndexByKey(this.defaultOptions.items, 'row_above'); + this.defaultOptions.items.splice(rowAboveIndex,1); + var rowBelowIndex = findIndexByKey(this.defaultOptions.items, 'row_above'); + this.defaultOptions.items.splice(rowBelowIndex,1); + this.defaultOptions.items.splice(rowBelowIndex,1); // FOR SEPARATOR + + } + + if(!this.instance.getSettings().allowInsertColumn) { + var colLeftIndex = findIndexByKey(this.defaultOptions.items, 'col_left'); + this.defaultOptions.items.splice(colLeftIndex,1); + var colRightIndex = findIndexByKey(this.defaultOptions.items, 'col_right'); + this.defaultOptions.items.splice(colRightIndex,1); + this.defaultOptions.items.splice(colRightIndex,1); // FOR SEPARATOR + + } + + var removeRow = false; + var removeCol = false; + var removeRowIndex, removeColumnIndex; + + if(!this.instance.getSettings().allowRemoveRow) { + removeRowIndex = findIndexByKey(this.defaultOptions.items, 'remove_row'); + this.defaultOptions.items.splice(removeRowIndex,1); + removeRow = true; + } + + if(!this.instance.getSettings().allowRemoveColumn) { + removeColumnIndex = findIndexByKey(this.defaultOptions.items, 'remove_col'); + this.defaultOptions.items.splice(removeColumnIndex,1); + removeCol = true; + } + + if (removeRow && removeCol) { + this.defaultOptions.items.splice(removeColumnIndex,1); // SEPARATOR + } + + 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); + + } + + /*** + * Create DOM instance of contextMenu + * @param menuName + * @param row + * @return {*} + */ + 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 = document.querySelector('.htContextMenu.' + menuName); + } else { + menu = document.querySelector('.htContextMenu'); + } + + + 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 () { + /* jshint ignore:start */ + function contextMenuOpenListener(event) { + var settings = this.instance.getSettings(); + + this.closeAll(); + + event.preventDefault(); + Handsontable.helper.stopPropagation(event); + + 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(settings.contextMenu); + + this.show(menu, items); + + this.setMenuPosition(event, menu); + + this.eventManager.addEventListener(document.documentElement, 'mousedown', Handsontable.helper.proxy(ContextMenu.prototype.closeAll, this)); + } + /* jshint ignore:end */ + var eventManager = Handsontable.eventManager(this.instance); + + eventManager.addEventListener(this.instance.rootElement, 'contextmenu', Handsontable.helper.proxy(contextMenuOpenListener, this)); + }; + + ContextMenu.prototype.bindTableEvents = function () { + this._afterScrollCallback = function () {}; + 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, hot) { + var contextMenu = this; + + 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(); + } + }; + + ContextMenu.prototype.unbindMouseEvents = function () { + this.eventManager.clear(); + var eventManager = Handsontable.eventManager(this.instance); + eventManager.removeEventListener(this.instance.rootElement, 'contextmenu'); + }; + + ContextMenu.prototype.show = function (menu, items) { + var that = this; + + menu.removeAttribute('style'); + menu.style.display = 'block'; + + var settings = { + data: items, + colHeaders: false, + colWidths: [200], + readOnly: true, + copyPaste: false, + columns: [ + { + data: 'name', + renderer: Handsontable.helper.proxy(this.renderer, this) + } + ], + renderAllRows: true, + beforeKeyDown: function (event) { + that.onBeforeKeyDown(event, htContextMenu); + }, + afterOnCellMouseOver: function (event, coords, TD) { + that.onCellMouseOver(event, coords, TD, htContextMenu); + } + }; + + var htContextMenu = new Handsontable(menu, settings); + + + this.eventManager.removeEventListener(menu, 'mousedown'); + this.eventManager.addEventListener(menu,'mousedown', function (event) { + that.performAction(event, htContextMenu); + }); + + this.bindTableEvents(); + htContextMenu.listen(); + + this.htMenus[htContextMenu.guid] = htContextMenu; + }; + + ContextMenu.prototype.close = function (menu) { + this.hide(menu); + this.eventManager.clear(); + 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); + } + + }; + + ContextMenu.prototype.hide = function (menu) { + menu.style.display = 'none'; + var instance =this.htMenus[menu.id]; + + instance.destroy(); + delete this.htMenus[menu.id]; + }; + + 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'); + + this.eventManager.addEventListener(wrapper, 'mouseenter', function () { + instance.deselectCell(); + }); + + } else { + if (isSubMenu(item)) { + Handsontable.Dom.addClass(TD, 'htSubmenu'); + + + this.eventManager.addEventListener(wrapper, 'mouseenter', function () { + instance.selectCell(row, col); + }); + + } else { + Handsontable.Dom.removeClass(TD, 'htSubmenu'); + Handsontable.Dom.removeClass(TD, 'htDisabled'); + + this.eventManager.addEventListener(wrapper, 'mouseenter', function () { + instance.selectCell(row, col); + }); + } + } + + + function isSubMenu(item) { + return item.hasOwnProperty('submenu'); + } + + function itemIsSeparator(item) { + return new RegExp(ContextMenu.SEPARATOR.name, '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, hot) { + var menusLength = this.menus.length; + + if (menusLength > 0) { + var lastMenu = this.menus[menusLength - 1]; + if (lastMenu.id != hot.guid) { + 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, instance) { + + Handsontable.Dom.enableImmediatePropagation(event); + var contextMenu = this; + + 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, instance); + } + 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 (instance.rootElement.className.indexOf('htContextSubMenu_') != -1) { + contextMenu.closeLastOpenedSubMenu(); + var index = contextMenu.menus.length; + + if (index > 0) { + var menu = contextMenu.menus[index - 1]; + + var triggerRow = contextMenu.triggerRows.pop(); + instance = this.htMenus[menu.id]; + 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(); + var subMenuInstance = contextMenu.show(subMenu, items); + + contextMenu.setSubMenuPosition(coords, subMenu); + subMenuInstance.selectCell(0, 0); + } + }; + + function findByKey(items, key) { + for (var i = 0, ilen = items.length; i < ilen; i++) { + if (items[i].key === key) { + return items[i]; + } + } + } + + function findIndexByKey(items, key) { + for (var i = 0, ilen = items.length; i < ilen; i++) { + if (items[i].key === key) { + return i; + } + } + } + + ContextMenu.prototype.getItems = function (items) { + var menu, item; + + function ContextMenuItem(rawItem) { + if (typeof rawItem == 'string') { + this.name = rawItem; + } else { + Handsontable.helper.extend(this, rawItem); + } + } + + ContextMenuItem.prototype = items; + + if (items && items.items) { + items = items.items; + } + + if (items === true) { + items = this.defaultOptions.items; + } + + if (1 == 1) { + menu = []; + for (var key in items) { + if (items.hasOwnProperty(key)) { + if (typeof items[key] === 'string') { + item = findByKey(this.defaultOptions.items, items[key]); + } + else { + item = findByKey(this.defaultOptions.items, key); + } + if (!item) { + item = items[key]; + } + item = new ContextMenuItem(item); + if (typeof items[key] === 'object') { + Handsontable.helper.extend(item, items[key]); + } + if (!item.key) { + item.key = key; + } + menu.push(item); + } + } + } + + return menu; + }; + + 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, document.body.clientWidth)) { + 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, document.body.clientWidth)) { + this.positionMenuOnRightOfCursor(cursor, menu, true); + } else { + this.positionMenuOnLeftOfCursor(cursor, menu, true); + } + }; + + ContextMenu.prototype.setMenuPosition = function (event, menu) { + // for ie8 + // http://msdn.microsoft.com/en-us/library/ie/ff974655(v=vs.85).aspx + var scrollTop = Handsontable.Dom.getWindowScrollTop(); + var scrollLeft = Handsontable.Dom.getWindowScrollLeft(); + var cursorY = event.pageY || (event.clientY + scrollTop); + var cursorX = event.pageX || (event.clientX + scrollLeft); + + 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, document.body.clientHeight)) { + this.positionMenuBelowCursor(cursor, menu); + } else { + if (this.menuFitsAboveCursor(cursor, menu)) { + this.positionMenuAboveCursor(cursor, menu); + } else { + this.positionMenuBelowCursor(cursor, menu); + } + } + + if (this.menuFitsOnRightOfCursor(cursor, menu, document.body.clientWidth)) { + 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, viewportHeight) { + return cursor.topRelative + menu.offsetHeight <= viewportHeight; + }; + + ContextMenu.prototype.menuFitsOnRightOfCursor = function (cursor, menu, viewportHeight) { + return cursor.leftRelative + menu.offsetWidth <= viewportHeight; + }; + + 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 = 1 + cursor.left + cursor.cellWidth + 'px'; + } else { + menu.style.left = 1 + 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.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 = document.querySelectorAll('.handsontable'); + var menuEnabled = false; + + for (var i = 0, len = hotContainers.length; i < len; i++) { + var instance = this.htMenus[hotContainers[i].id]; + if (instance && instance.getSettings().contextMenu) { + menuEnabled = true; + break; + } + } + + return menuEnabled; + }; + + ContextMenu.prototype.removeMenu = function (menu) { + if (menu.parentNode) { + this.menu.parentNode.removeChild(menu); + } + }; + + ContextMenu.SEPARATOR = {name: "---------"}; + + function updateHeight() { + /* jshint ignore:start */ + if (this.rootElement.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.name) { + realSeparatorHeight += 2; + } else { + realEntrySize += 26; + } + } + + this.view.wt.wtScrollbars.vertical.fixedContainer.style.height = realEntrySize + realSeparatorHeight + "px"; + /* jshint ignore:end */ + } + + function init() { + /* jshint ignore:start */ + var instance = this; + /* jshint ignore:end */ + 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(); + } 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); + + Handsontable.PluginHooks.register('afterContextMenuDefaultOptions'); + + Handsontable.ContextMenu = ContextMenu; + +})(Handsontable); + +function Comments(instance) { + + var eventManager = Handsontable.eventManager(instance), + 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) { + eventManager.removeEventListener(document, 'mouseover'); + if (!(event.target.className == 'htCommentTextArea' || event.target.innerHTML.indexOf('Comment') != -1)) { + var value = document.querySelector('.htCommentTextArea').value; + if (value.trim().length > 1) { + saveComment(range, value, instance); + } + unBindMouseEvent(); + hideCommentTextArea(); + } + } + + eventManager.addEventListener(document, 'mousedown',Handsontable.helper.proxy(commentsListener)); + }, + unBindMouseEvent = function () { + eventManager.removeEventListener(document, 'mousedown'); + eventManager.addEventListener(document, 'mousedown', 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.querySelector('.htComments'); + + 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.querySelector('.htCommentTextArea').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 () { + eventManager.addEventListener(document, 'mouseover', 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.push(Handsontable.ContextMenu.SEPARATOR); + + defaultOptions.items.push({ + key: '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.push({ + key: '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); + + +/** + * 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') + , eventManager = Handsontable.eventManager(this); + + 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.appendChild(handle); + } + } + + function refreshHandlePosition(TH, delta) { + var box = TH.getBoundingClientRect(); + var handleWidth = 6; + if (delta > 0) { + handle.style.left = (box.left + box.width - handleWidth) + 'px'; + } + else { + 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.appendChild(guide); + } + + function refreshGuidePosition(diff) { + guide.style.left = startOffset + diff + 'px'; + } + + function hideHandleAndGuide() { + Handsontable.Dom.removeClass(handle, 'active'); + Handsontable.Dom.removeClass(guide, 'active'); + } + + var checkColumnHeader = function (element) { + if (element.tagName != 'BODY') { + if (element.parentNode.tagName == 'THEAD') { + return true; + } else { + element = element.parentNode; + return checkColumnHeader(element); + } + } + return false; + }; + + var getTHFromTargetElement = function (element) { + if (element.tagName != 'TABLE') { + if (element.tagName == 'TH') { + return element; + } else { + return getTHFromTargetElement(element.parentNode); + } + } + return null; + }; + + var bindEvents = function () { + + var instance = this; + var pressed; + + eventManager.addEventListener(instance.rootElement,'mouseover',function (e) { + if (checkColumnHeader(e.target)){ + var th = getTHFromTargetElement(e.target); + if (th) { + if (pressed) { + var col = instance.view.wt.wtTable.getCoords(th).col; + if(col >= 0) { //not TH above row header + endCol = col; + refreshHandlePosition(e.target, endCol - startCol); + } + } + else { + setupHandlePosition.call(instance, th); + } + } + } + }); + + eventManager.addEventListener(instance.rootElement,'mousedown', function (e) { + if (Handsontable.Dom.hasClass(e.target, 'manualColumnMover')){ + startX = Handsontable.helper.pageX(e); + setupGuidePosition.call(instance); + pressed = instance; + + startCol = currentCol; + endCol = currentCol; + } + }); + + eventManager.addEventListener(window,'mousemove',function (e) { + if (pressed) { + refreshGuidePosition(Handsontable.helper.pageX(e) - startX); + } + }); + + + eventManager.addEventListener(window,'mouseup',function (e) { + if (pressed) { + hideHandleAndGuide(); + pressed = false; + + 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(){ + eventManager.clear(); + }; + + 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 (Array.isArray(initialManualColumnPositions)) { + this.manualColumnPositions = initialManualColumnPositions; + } else { + this.manualColumnPositions = []; + } + + if (source == 'afterInit') { + + // update plugin usages count for manualColumnPositions + if (typeof instance.manualColumnPositionsPluginUsages != 'undefined') { + instance.manualColumnPositionsPluginUsages.push('manualColumnMove'); + } else { + instance.manualColumnPositionsPluginUsages = ['manualColumnMove']; + } + + bindEvents.call(this); + if (this.manualColumnPositions.length > 0) { + this.forceFullRender = true; + this.render(); + } + } + + } else { + var pluginUsagesIndex = instance.manualColumnPositionsPluginUsages ? instance.manualColumnPositionsPluginUsages.indexOf('manualColumnMove') : -1; + if (pluginUsagesIndex > -1) { + unbindEvents.call(this); + this.manualColumnPositions = []; + instance.manualColumnPositionsPluginUsages[pluginUsagesIndex] = void 0; + } + } + }; + + 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; + }; + + // need to reconstruct manualcolpositions after removing columns + this.afterRemoveCol = function (index, amount) { + if (!this.getSettings().manualColumnMove) { + return; + } + + var rmindx, + colpos = this.manualColumnPositions; + + // We have removed columns, we also need to remove the indicies from manual column array + rmindx = colpos.splice(index, amount); + + // We need to remap manualColPositions so it remains constant linear from 0->ncols + colpos = colpos.map(function (colpos) { + var i, newpos = colpos; + + for (i = 0; i < rmindx.length; i++) { + if (colpos > rmindx[i]) { + newpos--; + } + } + + return newpos; + }); + + this.manualColumnPositions = colpos; + }; + + // need to reconstruct manualcolpositions after adding columns + this.afterCreateCol = function (index, amount) { + if (!this.getSettings().manualColumnMove) { + return; + } + + var colpos = this.manualColumnPositions; + if (!colpos.length) { + return; + } + + var addindx = []; + for (var i = 0; i < amount; i++) { + addindx.push(index + i); + } + + if (index >= colpos.length) { + colpos.concat(addindx); + } + else { + // We need to remap manualColPositions so it remains constant linear from 0->ncols + colpos = colpos.map(function (colpos) { + return (colpos >= index) ? (colpos + amount) : colpos; + }); + + // We have added columns, we also need to add new indicies to manualcolumn position array + colpos.splice.apply(colpos, [index, 0].concat(addindx)); + } + + this.manualColumnPositions = colpos; + }; +} +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.add('afterRemoveCol', htManualColumnMove.afterRemoveCol); +Handsontable.hooks.add('afterCreateCol', htManualColumnMove.afterCreateCol); +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') + , eventManager = Handsontable.eventManager(this); + + + 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.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.appendChild(guide); + } + + function refreshGuidePosition() { + guide.style.left = handle.style.left; + } + + function hideHandleAndGuide() { + Handsontable.Dom.removeClass(handle, 'active'); + Handsontable.Dom.removeClass(guide, 'active'); + } + + var checkColumnHeader = function (element) { + if (element.tagName != 'BODY') { + if (element.parentNode.tagName == 'THEAD') { + return true; + } else { + element = element.parentNode; + return checkColumnHeader(element); + } + } + return false; + }; + + var getTHFromTargetElement = function (element) { + if (element.tagName != 'TABLE') { + if (element.tagName == 'TH') { + return element; + } else { + return getTHFromTargetElement(element.parentNode); + } + } + return null; + }; + + var bindEvents = function () { + var instance = this; + var pressed; + var dblclick = 0; + var autoresizeTimeout = null; + + eventManager.addEventListener(instance.rootElement, 'mouseover', function (e) { + if (checkColumnHeader(e.target)) { + var th = getTHFromTargetElement(e.target); + if (th) { + if (!pressed) { + setupHandlePosition.call(instance, th); + } + } + } + }); + + eventManager.addEventListener(instance.rootElement, 'mousedown', function (e) { + if (Handsontable.Dom.hasClass(e.target, 'manualColumnResizer')) { + 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 = Handsontable.helper.pageX(e); + newSize = startWidth; + } + }); + + eventManager.addEventListener(window, 'mousemove', function (e) { + if (pressed) { + currentWidth = startWidth + (Handsontable.helper.pageX(e) - startX); + newSize = setManualSize(currentCol, currentWidth); //save col width + refreshHandlePosition(); + refreshGuidePosition(); + } + }); + + eventManager.addEventListener(window, 'mouseup', 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 () { + eventManager.clear(); + }; + + 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); + + // update plugin usages count for manualColumnPositions + if (typeof instance.manualColumnWidthsPluginUsages != 'undefined') { + instance.manualColumnWidthsPluginUsages.push('manualColumnResize'); + } else { + instance.manualColumnWidthsPluginUsages = ['manualColumnResize']; + } + + if (typeof loadedManualColumnWidths != 'undefined') { + this.manualColumnWidths = loadedManualColumnWidths; + } else if (Array.isArray(initialColumnWidths)) { + this.manualColumnWidths = initialColumnWidths; + } else { + this.manualColumnWidths = []; + } + + if (source == 'afterInit') { + bindEvents.call(this); + if (this.manualColumnWidths.length > 0) { + this.forceFullRender = true; + this.render(); + } + } + } + else { + var pluginUsagesIndex = instance.manualColumnWidthsPluginUsages ? instance.manualColumnWidthsPluginUsages.indexOf('manualColumnResize') : -1; + if (pluginUsagesIndex > -1) { + 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.run(instance, 'modifyCol', col); + instance.manualColumnWidths[col] = width; + + return width; + }; + + this.modifyColWidth = function (width, col) { + col = this.runHooks('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') + , eventManager = Handsontable.eventManager(this); + + 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.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.appendChild(guide); + } + + function refreshGuidePosition() { + guide.style.top = handle.style.top; + } + + function hideHandleAndGuide() { + Handsontable.Dom.removeClass(handle, 'active'); + Handsontable.Dom.removeClass(guide, 'active'); + } + + var checkRowHeader = function (element) { + if (element.tagName != 'BODY') { + if (element.parentNode.tagName == 'TBODY') { + return true; + } else { + element = element.parentNode; + return checkRowHeader(element); + } + } + return false; + }; + + var getTHFromTargetElement = function (element) { + if (element.tagName != 'TABLE') { + if (element.tagName == 'TH') { + return element; + } else { + return getTHFromTargetElement(element.parentNode); + } + } + return null; + }; + + var bindEvents = function () { + var instance = this; + var pressed; + var dblclick = 0; + var autoresizeTimeout = null; + + eventManager.addEventListener(instance.rootElement, 'mouseover', function (e) { + if (checkRowHeader(e.target)) { + var th = getTHFromTargetElement(e.target); + if (th) { + if (!pressed) { + setupHandlePosition.call(instance, th); + } + } + } + }); + + eventManager.addEventListener(instance.rootElement, 'mousedown', function (e) { + if (Handsontable.Dom.hasClass(e.target, 'manualRowResizer')) { + 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 = Handsontable.helper.pageY(e); + newSize = startHeight; + } + }); + + eventManager.addEventListener(window, 'mousemove', function (e) { + if (pressed) { + currentHeight = startHeight + (Handsontable.helper.pageY(e) - startY); + newSize = setManualSize(currentRow, currentHeight); + refreshHandlePosition(); + refreshGuidePosition(); + } + }); + + eventManager.addEventListener(window, 'mouseup', function (e) { + 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 () { + eventManager.clear(); + }; + + 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); + + // update plugin usages count for manualColumnPositions + if (typeof instance.manualRowHeightsPluginUsages != 'undefined') { + instance.manualRowHeightsPluginUsages.push('manualRowResize'); + } else { + instance.manualRowHeightsPluginUsages = ['manualRowResize']; + } + + if (typeof loadedManualRowHeights != 'undefined') { + this.manualRowHeights = loadedManualRowHeights; + } else if (Array.isArray(initialRowHeights)) { + 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 { + var pluginUsagesIndex = instance.manualRowHeightsPluginUsages ? instance.manualRowHeightsPluginUsages.indexOf('manualRowResize') : -1; + if (pluginUsagesIndex > -1) { + unbindEvents.call(this); + this.manualRowHeights = []; + instance.manualRowHeightsPluginUsages[pluginUsagesIndex] = void 0; + } + } + }; + + var setManualSize = function (row, height) { + row = Handsontable.hooks.run(instance, 'modifyRow', row); + instance.manualRowHeights[row] = height; + + return height; + }; + + this.modifyRowHeight = function (height, row) { + if (this.getSettings().manualRowResize) { + row = this.runHooks('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.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(Array.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 = Handsontable.helper.deepClone(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 = Handsontable.helper.deepClone(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 { + dragToScroll.setBoundaries(scrollHandler.getBoundingClientRect()); + } + + dragToScroll.setCallback(function (scrollX, scrollY) { + if (scrollX < 0) { + scrollHandler.scrollLeft -= 50; + } + else if (scrollX > 0) { + scrollHandler.scrollLeft += 50; + } + + if (scrollY < 0) { + scrollHandler.scrollTop -= 20; + } + else if (scrollY > 0) { + scrollHandler.scrollTop += 20; + } + }); + + instance.dragToScrollListening = true; + }; + + Handsontable.hooks.add('afterInit', function () { + var instance = this; + var eventManager = Handsontable.eventManager(this); + + eventManager.addEventListener(document,'mouseup', function () { + instance.dragToScrollListening = false; + }); + + eventManager.addEventListener(document,'mousemove', function (event) { + if (instance.dragToScrollListening) { + dragToScroll.check(event.clientX, event.clientY); + } + }); + }); + + Handsontable.hooks.add('afterDestroy', function () { + var eventManager = Handsontable.eventManager(this); + eventManager.clear(); + }); + + 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) { + var _this = this; + + this.copyPasteInstance = CopyPaste.getInstance(); + this.copyPasteInstance.onCut(onCut); + this.copyPasteInstance.onPaste(onPaste); + + instance.addHook('beforeKeyDown', onBeforeKeyDown); + + function onCut() { + if (!instance.isListening()) { + return; + } + instance.selection.empty(); + } + + function onPaste(str) { + var + input, + inputArray, + selected, + coordsFrom, + coordsTo, + cellRange, + topLeftCorner, + bottomRightCorner, + areaStart, + areaEnd; + + if (!instance.isListening() || !instance.selection.isSelected()) { + return; + } + input = str; + 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) { + var ctrlDown; + + if (instance.getSelected()) { + if (Handsontable.helper.isCtrlKey(event.keyCode)) { + // when CTRL is pressed, prepare selectable text in textarea + // http://stackoverflow.com/questions/3902635/how-does-one-capture-a-macs-command-key-via-javascript + _this.setCopyableText(); + event.stopImmediatePropagation(); + + return; + } + // catch CTRL but not right ALT (which in some systems triggers ALT+CTRL) + ctrlDown = (event.ctrlKey || event.metaKey) && !event.altKey; + + if (event.keyCode == Handsontable.helper.keyCode.A && ctrlDown) { + instance._registerTimeout(setTimeout(Handsontable.helper.proxy(_this.setCopyableText, _this), 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, + 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() { + /* jshint ignore:start */ + var instance = this; + /* jshint ignore:end */ + + 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 (Array.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.removeAttribute('rowspan'); + TD.removeAttribute('colspan'); + + TD.style.display = "none"; + } + } + else { + TD.removeAttribute('rowspan'); + TD.removeAttribute('colspan'); + } +}; + +MergeCells.prototype.modifyTransform = function (hook, currentSelectedRange, delta) { + var sameRowspan = function (merged, coords) { + if (coords.row >= merged.row && coords.row <= (merged.row + merged.rowspan - 1)) { + return true; + } + return false; + } + , sameColspan = function (merged, coords) { + if (coords.col >= merged.col && coords.col <= (merged.col + merged.colspan - 1)) { + return true; + } + return false; + } + , getNextPosition = function (newDelta) { + return new WalkontableCellCoords(currentSelectedRange.to.row + newDelta.row, currentSelectedRange.to.col + newDelta.col); + }; + + var newDelta = { + row: delta.row, + col: delta.col + }; + + + if (hook == 'modifyTransformStart') { + + if (!this.lastDesiredCoords) { + this.lastDesiredCoords = new WalkontableCellCoords(null, null); + } + var currentPosition = new WalkontableCellCoords(currentSelectedRange.highlight.row, currentSelectedRange.highlight.col) + , mergedParent = this.mergedCellInfoCollection.getInfo(currentPosition.row, currentPosition.col)// if current position's parent is a merged range, returns it + , currentRangeContainsMerge; // if current range contains a merged range + + for (var i = 0, mergesLength = this.mergedCellInfoCollection.length; i < mergesLength; i++) { + var range = this.mergedCellInfoCollection[i]; + range = new WalkontableCellCoords(range.row + range.rowspan - 1, range.col + range.colspan - 1); + if (currentSelectedRange.includes(range)) { + currentRangeContainsMerge = true; + break; + } + } + + if (mergedParent) { // only merge selected + var mergeTopLeft = new WalkontableCellCoords(mergedParent.row, mergedParent.col) + , mergeBottomRight = new WalkontableCellCoords(mergedParent.row + mergedParent.rowspan - 1, mergedParent.col + mergedParent.colspan - 1) + , mergeRange = new WalkontableCellRange(mergeTopLeft, mergeTopLeft, mergeBottomRight); + + if (!mergeRange.includes(this.lastDesiredCoords)) { + this.lastDesiredCoords = new WalkontableCellCoords(null, null); // reset outdated version of lastDesiredCoords + } + + newDelta.row = this.lastDesiredCoords.row ? this.lastDesiredCoords.row - currentPosition.row : newDelta.row; + newDelta.col = this.lastDesiredCoords.col ? this.lastDesiredCoords.col - currentPosition.col : newDelta.col; + + if (delta.row > 0) { // moving down + newDelta.row = mergedParent.row + mergedParent.rowspan - 1 - currentPosition.row + delta.row; + } else if (delta.row < 0) { //moving up + newDelta.row = currentPosition.row - mergedParent.row + delta.row; + } + if (delta.col > 0) { // moving right + newDelta.col = mergedParent.col + mergedParent.colspan - 1 - currentPosition.col + delta.col; + } else if (delta.col < 0) { // moving left + newDelta.col = currentPosition.col - mergedParent.col + delta.col; + } + } + + var nextPosition = new WalkontableCellCoords(currentSelectedRange.highlight.row + newDelta.row, currentSelectedRange.highlight.col + newDelta.col) + , nextParentIsMerged = this.mergedCellInfoCollection.getInfo(nextPosition.row, nextPosition.col); + + if (nextParentIsMerged) { // skipping the invisible cells in the merge range + this.lastDesiredCoords = nextPosition; + newDelta = { + row: nextParentIsMerged.row - currentPosition.row, + col: nextParentIsMerged.col - currentPosition.col + }; + } + } else if (hook == 'modifyTransformEnd') { + for (var i = 0, mergesLength = this.mergedCellInfoCollection.length; i < mergesLength; i++) { + var currentMerge = this.mergedCellInfoCollection[i] + , mergeTopLeft = new WalkontableCellCoords(currentMerge.row, currentMerge.col) + , mergeBottomRight = new WalkontableCellCoords(currentMerge.row + currentMerge.rowspan - 1, currentMerge.col + currentMerge.colspan - 1) + , mergedRange = new WalkontableCellRange(mergeTopLeft, mergeTopLeft, mergeBottomRight) + , sharedBorders = currentSelectedRange.getBordersSharedWith(mergedRange); + + if (mergedRange.isEqual(currentSelectedRange)) { // only the merged range is selected + currentSelectedRange.setDirection("NW-SE"); + } + else if (sharedBorders.length > 0) { + var mergeHighlighted = (currentSelectedRange.highlight.isEqual(mergedRange.from)); + + if (sharedBorders.indexOf('top') > -1) { // if range shares a border with the merged section, change range direction accordingly + if (currentSelectedRange.to.isSouthEastOf(mergedRange.from) && mergeHighlighted) { + currentSelectedRange.setDirection("NW-SE"); + } else if (currentSelectedRange.to.isSouthWestOf(mergedRange.from) && mergeHighlighted) { + currentSelectedRange.setDirection("NE-SW"); + } + } else if (sharedBorders.indexOf('bottom') > -1) { + if (currentSelectedRange.to.isNorthEastOf(mergedRange.from) && mergeHighlighted) { + currentSelectedRange.setDirection("SW-NE"); + } else if (currentSelectedRange.to.isNorthWestOf(mergedRange.from) && mergeHighlighted) { + currentSelectedRange.setDirection("SE-NW"); + } + } + } + + var nextPosition = getNextPosition(newDelta) + , withinRowspan = sameRowspan(currentMerge, nextPosition) + , withinColspan = sameColspan(currentMerge, nextPosition); + + if (currentSelectedRange.includesRange(mergedRange) && (mergedRange.includes(nextPosition) || withinRowspan || withinColspan)) { // if next step overlaps a merged range, jump past it + if (withinRowspan) { + if (newDelta.row < 0) { + newDelta.row -= currentMerge.rowspan - 1; + } else if (newDelta.row > 0) { + newDelta.row += currentMerge.rowspan - 1; + } + } + if (withinColspan) { + if (newDelta.col < 0) { + newDelta.col -= currentMerge.colspan - 1; + } else if (newDelta.col > 0) { + newDelta.col += currentMerge.colspan - 1; + } + } + } + } + } + + if (newDelta.row !== 0) { + delta.row = newDelta.row; + } + if (newDelta.col !== 0) { + delta.col = newDelta.col; + } +}; + +if (typeof Handsontable == 'undefined') { + throw new Error('Handsontable is not defined'); +} + +var beforeInit = function () { + var instance = this; + var mergeCellsSetting = instance.getSettings().mergeCells; + + if (mergeCellsSetting) { + if (!instance.mergeCells) { + instance.mergeCells = new MergeCells(mergeCellsSetting); + } + } +}; + +var afterInit = function () { + var instance = this; + if (instance.mergeCells) { + /** + * Monkey patch WalkontableTable.prototype.getCell to return TD for merged cell parent if asked for TD of a cell that is + * invisible due to the merge. This is not the cleanest solution but there is a test case for it (merged cells scroll) so feel free to refactor it! + */ + instance.view.wt.wtTable.getCell = function (coords) { + if (instance.getSettings().mergeCells) { + var mergeParent = instance.mergeCells.mergedCellInfoCollection.getInfo(coords.row, coords.col); + if (mergeParent) { + coords = mergeParent; + } + } + return WalkontableTable.prototype.getCell.call(this, coords); + }; + } +}; + +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.push(Handsontable.ContextMenu.SEPARATOR); + + defaultOptions.items.push({ + key: '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; + + var rangeExpanded = false; + do { + rangeExpanded = false; + + 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)) { + coords.row = selRange.to.row; + coords.col = selRange.to.col; + + rangeExpanded = true; + } + } + } while (rangeExpanded); + + } +}; + +/** + * Returns correct coordinates for merged start / end cells in selection for area borders + * @param corners + * @param className + */ +var beforeDrawAreaBorders = function (corners, className) { + if (className && className == 'area') { + var mergeCellsSetting = this.getSettings().mergeCells; + if (mergeCellsSetting) { + var selRange = this.getSelectedRange(); + var startRange = new WalkontableCellRange(selRange.from, selRange.from, selRange.from); + var stopRange = new WalkontableCellRange(selRange.to, selRange.to, selRange.to); + + 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 (startRange.expandByRange(mergedCellRange)) { + corners[0] = startRange.from.row; + corners[1] = startRange.from.col; + } + + if (stopRange.expandByRange(mergedCellRange)) { + corners[2] = stopRange.from.row; + corners[3] = stopRange.from.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; + } + } +}; + +var afterViewportRowCalculatorOverride = function (calc) { + var mergeCellsSetting = this.getSettings().mergeCells; + if (mergeCellsSetting) { + var colCount = this.countCols(); + var mergeParent; + for (var c = 0; c < colCount; c++) { + mergeParent = this.mergeCells.mergedCellInfoCollection.getInfo(calc.startRow, c); + if (mergeParent) { + if (mergeParent.row < calc.startRow) { + calc.startRow = mergeParent.row; + return afterViewportRowCalculatorOverride.call(this, calc); //recursively search upwards + } + } + mergeParent = this.mergeCells.mergedCellInfoCollection.getInfo(calc.endRow, c); + if (mergeParent) { + var mergeEnd = mergeParent.row + mergeParent.rowspan - 1; + if (mergeEnd > calc.endRow) { + calc.endRow = mergeEnd; + return afterViewportRowCalculatorOverride.call(this, calc); //recursively search upwards + } + } + } + } +}; + +var afterViewportColumnCalculatorOverride = function (calc) { + var mergeCellsSetting = this.getSettings().mergeCells; + if (mergeCellsSetting) { + var rowCount = this.countRows(); + var mergeParent; + for (var r = 0; r < rowCount; r++) { + mergeParent = this.mergeCells.mergedCellInfoCollection.getInfo(r, calc.startColumn); + + if (mergeParent) { + if (mergeParent.col < calc.startColumn) { + calc.startColumn = mergeParent.col; + return afterViewportColumnCalculatorOverride.call(this, calc); //recursively search upwards + } + } + mergeParent = this.mergeCells.mergedCellInfoCollection.getInfo(r, calc.endColumn); + if (mergeParent) { + var mergeEnd = mergeParent.col + mergeParent.colspan - 1; + if (mergeEnd > calc.endColumn) { + calc.endColumn = mergeEnd; + return afterViewportColumnCalculatorOverride.call(this, calc); //recursively search upwards + } + } + } + } +}; + +var isMultipleSelection = function (isMultiple) { + if (isMultiple && this.mergeCells) { + var mergedCells = this.mergeCells.mergedCellInfoCollection + , selectionRange = this.getSelectedRange(); + + for (var group in mergedCells) { + if (selectionRange.highlight.row == mergedCells[group].row && selectionRange.highlight.col == mergedCells[group].col && + selectionRange.to.row == mergedCells[group].row + mergedCells[group].rowspan - 1 && + selectionRange.to.col == mergedCells[group].col + mergedCells[group].colspan - 1) { + return false; + } + } + } + return isMultiple; +}; + +Handsontable.hooks.add('beforeInit', beforeInit); +Handsontable.hooks.add('afterInit', afterInit); +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('beforeDrawBorders', beforeDrawAreaBorders); +Handsontable.hooks.add('afterIsMultipleSelection', isMultipleSelection); +Handsontable.hooks.add('afterRenderer', afterRenderer); +Handsontable.hooks.add('afterContextMenuDefaultOptions', addMergeActionsToContextMenu); +Handsontable.hooks.add('afterGetCellMeta', afterGetCellMeta); +Handsontable.hooks.add('afterViewportRowCalculatorOverride', afterViewportRowCalculatorOverride); +Handsontable.hooks.add('afterViewportColumnCalculatorOverride', afterViewportColumnCalculatorOverride); + +Handsontable.MergeCells = MergeCells; + + +(function () { + + function CustomBorders () { + + } + +// /*** +// * Array for all custom border objects (for redraw) +// * @type {{}} +// */ +// var bordersArray = {}, + /*** + * Current instance (table where borders should be placed) + */ + var instance; + + + /*** + * Check if plugin should be enabled + */ + var checkEnable = function (customBorders) { + if(typeof customBorders === "boolean"){ + if (customBorders === true){ + return true; + } + } + + if(typeof customBorders === "object"){ + if(customBorders.length > 0) { + return true; + } + } + return false; + }; + + + /*** + * Initialize plugin + */ + var init = function () { + + if(checkEnable(this.getSettings().customBorders)){ + if(!this.customBorders){ + instance = this; + this.customBorders = new CustomBorders(); + } + } + }; + + /*** + * get index of border setting + * @param className + * @returns {number} + */ + var getSettingIndex = function (className) { + for (var i = 0; i < instance.view.wt.selections.length; i++){ + if (instance.view.wt.selections[i].settings.className == className){ + return i; + } + } + return -1; + }; + + /*** + * Insert WalkontableSelection instance into Walkontable.settings + * @param border + */ + var insertBorderIntoSettings = function (border) { + var coordinates = { + row: border.row, + col: border.col + }; + var selection = new WalkontableSelection(border, new WalkontableCellRange(coordinates, coordinates, coordinates)); + var index = getSettingIndex(border.className); + + if(index >=0) { + instance.view.wt.selections[index] = selection; + } else { + instance.view.wt.selections.push(selection); + } + }; + + /*** + * 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); + + insertBorderIntoSettings(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); + insertBorderIntoSettings(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; + }; + + /*** + * Remove borders divs from DOM + * + * @param borderClassName + */ + var removeBordersFromDom = function (borderClassName) { + var borders = document.querySelectorAll("." + borderClassName); + + for(var i = 0; i< borders.length; i++) { + if (borders[i]) { + if(borders[i].nodeName != 'TD') { + var parent = borders[i].parentNode; + if(parent.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); + this.removeCellMeta(row, col, 'borders'); + }; + + /*** + * 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; + /* jshint ignore:start */ + if (!bordersMeta || bordersMeta.border == undefined){ + bordersMeta = createEmptyBorders(row, col); + } + /* jshint ignore:end */ + + if (remove) { + bordersMeta[place] = createSingleEmptyBorder(); + } else { + bordersMeta[place] = createDefaultCustomBorder(); + } + + this.setCellMeta(row, col, 'borders', bordersMeta); + + var borderClassName = createClassName(row,col); + removeBordersFromDom(borderClassName); + insertBorderIntoSettings(bordersMeta); + + 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 "" + String.fromCharCode(10003) + "" + label; // workaround for https://github.com/handsontable/handsontable/issues/1946 + }; + + /*** + * Add border options to context menu + * + * @param defaultOptions + */ + var addBordersOptionsToContextMenu = function (defaultOptions) { + if(!this.getSettings().customBorders){ + return; + } + + defaultOptions.items.push(Handsontable.ContextMenu.SEPARATOR); + + defaultOptions.items.push({ + key: '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, 'left'); + 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('afterInit', function () { + var customBorders = this.getSettings().customBorders; + + if (customBorders){ + + 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]); + } + } + + this.render(); + this.view.wt.draw(true); + } + + }); + + 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'), + eventManager = Handsontable.eventManager(this); + + 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.appendChild(handle); + } + } + + function refreshHandlePosition(TH, delta) { + var box = TH.getBoundingClientRect(); + var handleHeight = 6; + if (delta > 0) { + handle.style.top = (box.top + box.height - handleHeight) + 'px'; + } + else { + 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.appendChild(guide); + } + + function refreshGuidePosition(diff) { + guide.style.top = startOffset + diff + 'px'; + } + + function hideHandleAndGuide() { + Handsontable.Dom.removeClass(handle, 'active'); + Handsontable.Dom.removeClass(guide, 'active'); + } + + var checkRowHeader = function (element) { + if (element.tagName != 'BODY') { + if (element.parentNode.tagName == 'TBODY') { + return true; + } else { + element = element.parentNode; + return checkRowHeader(element); + } + } + return false; + }; + + var getTHFromTargetElement = function (element) { + if (element.tagName != 'TABLE') { + if (element.tagName == 'TH') { + return element; + } else { + return getTHFromTargetElement(element.parentNode); + } + } + return null; + }; + + var bindEvents = function () { + var instance = this; + var pressed; + + + eventManager.addEventListener(instance.rootElement, 'mouseover', function (e) { + if (checkRowHeader(e.target)) { + var th = getTHFromTargetElement(e.target); + if (th) { + if (pressed) { + endRow = instance.view.wt.wtTable.getCoords(th).row; + refreshHandlePosition(th, endRow - startRow); + } + else { + setupHandlePosition.call(instance, th); + } + } + } + }); + + eventManager.addEventListener(instance.rootElement, 'mousedown', function (e) { + if (Handsontable.Dom.hasClass(e.target, 'manualRowMover')) { + startY = Handsontable.helper.pageY(e); + setupGuidePosition.call(instance); + pressed = instance; + + startRow = currentRow; + endRow = currentRow; + } + }); + + eventManager.addEventListener(window, 'mousemove', function (e) { + if (pressed) { + refreshGuidePosition(Handsontable.helper.pageY(e) - startY); + } + }); + + eventManager.addEventListener(window, 'mouseup', function (e) { + if (pressed) { + hideHandleAndGuide(); + pressed = false; + + 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 () { + eventManager.clear(); + }; + + 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); + + // update plugin usages count for manualColumnPositions + if (typeof instance.manualRowPositionsPluginUsages != 'undefined') { + instance.manualRowPositionsPluginUsages.push('manualColumnMove'); + } else { + instance.manualRowPositionsPluginUsages = ['manualColumnMove']; + } + + if (typeof loadedManualRowPostions != 'undefined') { + this.manualRowPositions = loadedManualRowPostions; + } else if (Array.isArray(initialManualRowPositions)) { + this.manualRowPositions = initialManualRowPositions; + } else { + this.manualRowPositions = []; + } + + if (source === 'afterInit') { + bindEvents.call(this); + if (this.manualRowPositions.length > 0) { + instance.forceFullRender = true; + instance.render(); + } + } + } else { + var pluginUsagesIndex = instance.manualRowPositionsPluginUsages ? instance.manualRowPositionsPluginUsages.indexOf('manualColumnMove') : -1; + if (pluginUsagesIndex > -1) { + unbindEvents.call(this); + instance.manualRowPositions = []; + instance.manualRowPositionsPluginUsages[pluginUsagesIndex] = void 0; + } + } + + }; + + 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 wtOnCellCornerMouseDown, + wtOnCellMouseOver, + mouseDownOnCellCorner = false, + plugin = this, + eventManager = Handsontable.eventManager(instance); + + + var mouseUpCallback = function (event) { + if (!instance.autofill) { + return true; + } + + if (instance.autofill.handle && instance.autofill.handle.isDragged) { + if (instance.autofill.handle.isDragged > 1) { + instance.autofill.apply(); + } + instance.autofill.handle.isDragged = 0; + mouseDownOnCellCorner = false; + } + }; + + eventManager.addEventListener(document, 'mouseup', function (event) { + mouseUpCallback(event); + }); + + eventManager.addEventListener(document,'mousemove', function (event){ + if (!plugin.instance.autofill) { + return 0; + } + + var tableBottom = Handsontable.Dom.offset(plugin.instance.table).top - (window.pageYOffset || document.documentElement.scrollTop) + Handsontable.Dom.outerHeight(plugin.instance.table) + , tableRight = Handsontable.Dom.offset(plugin.instance.table).left - (window.pageXOffset || document.documentElement.scrollLeft) + Handsontable.Dom.outerWidth(plugin.instance.table); + + + if (plugin.addingStarted === false && plugin.instance.autofill.handle.isDragged > 0 && event.clientY > tableBottom && event.clientX <= tableRight) { // dragged outside bottom + this.mouseDragOutside = true; + plugin.addingStarted = true; + } else { + this.mouseDragOutside = false; + } + + if (this.mouseDragOutside) { + setTimeout(function () { + plugin.addingStarted = false; + plugin.instance.alter('insert_row'); + }, 200); + } + }); + + /* + * 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; + mouseDownOnCellCorner = true; + + 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 && (mouseDownOnCellCorner && !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; + } + + var getDeltas = function (start, end, data, direction) { + var rlength = data.length, // rows + clength = data ? data[0].length : 0; // cols + + var deltas = []; + + var diffRow = end.row - start.row, + diffCol = end.col - start.col; + + var startValue, endValue, delta; + + var arr = []; + + if (['down', 'up'].indexOf(direction) !== -1) { + for (var col = 0; col <= diffCol; col++) { + + startValue = parseInt(data[0][col], 10); + endValue = parseInt(data[rlength-1][col], 10); + delta = (direction === 'down' ? (endValue - startValue) : (startValue - endValue)) / (rlength - 1) || 0; + + arr.push(delta); + } + + deltas.push(arr); + } + + if (['right', 'left'].indexOf(direction) !== -1) { + for (var row = 0; row <= diffRow; row++) { + + startValue = parseInt(data[row][0], 10); + endValue = parseInt(data[row][clength-1], 10); + delta = (direction === 'right' ? (endValue - startValue) : (startValue - endValue)) / (clength - 1) || 0; + + arr = []; + arr.push(delta); + + deltas.push(arr); + } + } + + return deltas; + }; + + 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(); + } + + var direction; + + if (drag[0] === select[0] && drag[1] < select[1]) { + direction = 'left'; + + 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]) { + direction = 'right'; + + 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]) { + direction = 'up'; + + 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]) { + direction = 'down'; + + start = new WalkontableCellCoords( + select[2] + 1, + drag[1] + ); + end = new WalkontableCellCoords( + drag[2], + drag[3] + ); + } + + if (start && start.row > -1 && start.col > -1) { + 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); + + var deltas = getDeltas(start, end, _data, direction); + + Handsontable.hooks.run(this.instance, 'beforeAutofill', start, end, _data); + + this.instance.populateFromArray(start.row, start.col, _data, end.row, end.col, 'autofill', null, direction, deltas); + + 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 + this.instance.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, + selection, + tableRows = this.instance.countRows(), + that = this; + + if (this.instance.view.wt.selections.fill.cellRange && this.addingStarted === false) { + selection = this.instance.getSelected(); + fillCorners = this.instance.view.wt.selections.fill.getCorners(); + + if (selection[2] < tableRows - 1 && 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); + +var Grouping = function (instance) { + /** + * array of items + * @type {Array} + */ + var groups = []; + + /** + * group definition + * @type {{id: String, level: Number, rows: Array, cols: Array, hidden: Number}} + */ + var item = { + id: '', + level: 0, + hidden: 0, + rows: [], + cols: [] + }; + + /** + * total rows and cols merged in groups + * @type {{rows: number, cols: number}} + */ + var counters = { + rows: 0, + cols: 0 + }; + + /** + * Number of group levels in each dimension + * @type {{rows: number, cols: number}} + */ + var levels = { + rows: 0, + cols: 0 + }; + + /** + * List of hidden rows + * @type {Array} + */ + var hiddenRows = []; + + /** + * List of hidden columns + * @type {Array} + */ + var hiddenCols = []; + + /** + * Classes used + */ + var classes = { + 'groupIndicatorContainer': 'htGroupIndicatorContainer', + 'groupIndicator': function (direction) { + return 'ht' + direction + 'Group'; + }, + 'groupStart': 'htGroupStart', + 'collapseButton': 'htCollapseButton', + 'expandButton': 'htExpandButton', + 'collapseGroupId': function (id) { + return 'htCollapse-' + id; + }, + 'collapseFromLevel': function (direction, level) { + return 'htCollapse' + direction + 'FromLevel-' + level; + }, + 'clickable': 'clickable', + 'levelTrigger': 'htGroupLevelTrigger' + }; + + /** + * compare object properties + * @param {String} property + * @param {String} orderDirection + * @returns {Function} + */ + var compare = function (property, orderDirection) { + return function (item1, item2) { + return typeof (orderDirection) === 'undefined' || orderDirection === 'asc' ? item1[property] - item2[property] : item2[property] - item1[property]; + }; + }; + + /** + * Create range array between from and to + * @param {Number} from + * @param {Number} to + * @returns {Array} + */ + var range = function (from, to) { + var arr = []; + while (from <= to) { + arr.push(from++); + } + + return arr; + }; + + /** + * * Get groups for range + * @param from + * @param to + * @returns {{total: {rows: number, cols: number}, groups: Array}} + */ + var getRangeGroups = function (dataType, from, to) { + var cells = [], + cell = { + row: null, + col: null + }; + + if (dataType == "cols") { + // get all rows for selected columns + while (from <= to) { + cell = { + row: -1, + col: from++ + }; + cells.push(cell); + } + + } else { + // get all columns for selected rows + while (from <= to) { + cell = { + row: from++, + col: -1 + }; + cells.push(cell); + } + } + + var cellsGroups = getCellsGroups(cells), + totalRows = 0, + totalCols = 0; + + // for selected cells, calculate total groups divided into rows and columns + for (var i = 0; i < cellsGroups.length; i++) { + totalRows += cellsGroups[i].filter(function (item) { + return item['rows']; + }).length; + + totalCols += cellsGroups[i].filter(function (item) { + return item['cols']; + }).length; + } + + return { + total: { + rows: totalRows, + cols: totalCols + }, + groups: cellsGroups + }; + }; + + /** + * Get all groups for cells + * @param {Array} cells [{row:0, col:0}, {row:0, col:1}, {row:1, col:2}] + * @returns {Array} + */ + var getCellsGroups = function (cells) { + var _groups = []; + + for (var i = 0; i < cells.length; i++) { + _groups.push(getCellGroups(cells[i])); + } + + return _groups; + }; + + /** + * Get all groups for cell + * @param {Object} coords {row:1, col:2} + * @param {Number} groupLevel Optional + * @param {String} groupType Optional + * @returns {Array} + */ + var getCellGroups = function (coords, groupLevel, groupType) { + var row = coords.row, + col = coords.col; + + // for row = -1 and col = -1, get all columns and rows + var tmpRow = (row === -1 ? 0 : row), + tmpCol = (col === -1 ? 0 : col); + + var _groups = []; + + for (var i = 0; i < groups.length; i++) { + var group = groups[i], + id = group['id'], + level = group['level'], + rows = group['rows'] || [], + cols = group['cols'] || []; + + if (_groups.indexOf(id) === -1) { + if (rows.indexOf(tmpRow) !== -1 || cols.indexOf(tmpCol) !== -1) { + _groups.push(group); + } + } + } + + // add col groups + if (col === -1) { + _groups = _groups.concat(getColGroups()); + } else if (row === -1) { + // add row groups + _groups = _groups.concat(getRowGroups()); + } + + if (groupLevel) { + _groups = _groups.filter(function (item) { + return item['level'] === groupLevel; + }); + } + + if (groupType) { + if (groupType === 'cols') { + _groups = _groups.filter(function (item) { + return item['cols']; + }); + } else if (groupType === 'rows') { + _groups = _groups.filter(function (item) { + return item['rows']; + }); + } + } + + // remove duplicates + var tmp = []; + return _groups.filter(function (item) { + if (tmp.indexOf(item.id) === -1) { + tmp.push(item.id); + return item; + } + }); + }; + + /** + * get group by id + * @param id + * @returns {Object} group + */ + var getGroupById = function (id) { + for (var i = 0, groupsLength = groups.length; i < groupsLength; i++) { + if (groups[i].id == id) { + return groups[i]; + } + } + return false; + }; + + /** + * get group by row and level + * @param row + * @param level + * @returns {Object} group + */ + var getGroupByRowAndLevel = function (row, level) { + for (var i = 0, groupsLength = groups.length; i < groupsLength; i++) { + if (groups[i].level == level && groups[i].rows && groups[i].rows.indexOf(row) > -1) { + return groups[i]; + } + } + return false; + }; + + /** + * get group by row and level + * @param row + * @param level + * @returns {Object} group + */ + var getGroupByColAndLevel = function (col, level) { + for (var i = 0, groupsLength = groups.length; i < groupsLength; i++) { + if (groups[i].level == level && groups[i].cols && groups[i].cols.indexOf(col) > -1) { + return groups[i]; + } + } + return false; + }; + + /** + * get total column groups + * @returns {*|Array} + */ + var getColGroups = function () { + var result = []; + for (var i = 0, groupsLength = groups.length; i < groupsLength; i++) { + if (Array.isArray(groups[i]['cols'])) { + result.push(groups[i]); + } + } + return result; + }; + + /** + * get total col groups by level + * @param {Number} level + * @returns {*|Array} + */ + var getColGroupsByLevel = function (level) { + var result = []; + for (var i = 0, groupsLength = groups.length; i < groupsLength; i++) { + if (groups[i]['cols'] && groups[i]['level'] === level) { + result.push(groups[i]); + } + } + return result; + }; + + /** + * get total row groups + * @returns {*|Array} + */ + var getRowGroups = function () { + var result = []; + for (var i = 0, groupsLength = groups.length; i < groupsLength; i++) { + if (Array.isArray(groups[i]['rows'])) { + result.push(groups[i]); + } + } + return result; + }; + + /** + * get total row groups by level + * @param {Number} level + * @returns {*|Array} + */ + var getRowGroupsByLevel = function (level) { + var result = []; + for (var i = 0, groupsLength = groups.length; i < groupsLength; i++) { + if (groups[i]['rows'] && groups[i]['level'] === level) { + result.push(groups[i]); + } + } + return result; + }; + + /** + * get last inserted range level in columns + * @param {Array} rangeGroups + * @returns {number} + */ + var getLastLevelColsInRange = function (rangeGroups) { + var level = 0; + + if (rangeGroups.length) { + rangeGroups.forEach(function (items) { + items = items.filter(function (item) { + return item['cols']; + }); + + if (items.length) { + var sortedGroup = items.sort(compare('level', 'desc')), + lastLevel = sortedGroup[0].level; + + if (level < lastLevel) { + level = lastLevel; + } + } + }); + } + + return level; + }; + + /** + * get last inserted range level in rows + * @param {Array} rangeGroups + * @returns {number} + */ + var getLastLevelRowsInRange = function (rangeGroups) { + var level = 0; + + if (rangeGroups.length) { + rangeGroups.forEach(function (items) { + items = items.filter(function (item) { + return item['rows']; + }); + + if (items.length) { + var sortedGroup = items.sort(compare('level', 'desc')), + lastLevel = sortedGroup[0].level; + + if (level < lastLevel) { + level = lastLevel; + } + } + }); + } + + return level; + }; + + /** + * create group for cols + * @param {Number} from + * @param {Number} to + */ + var groupCols = function (from, to) { + var rangeGroups = getRangeGroups("cols", from, to), + lastLevel = getLastLevelColsInRange(rangeGroups.groups); + + if (lastLevel === levels.cols) { + levels.cols++; + } else if (lastLevel > levels.cols) { + levels.cols = lastLevel + 1; + } + + if (!counters.cols) { + counters.cols = getColGroups().length; + } + + counters.cols++; + groups.push({ + id: 'c' + counters.cols, + level: lastLevel + 1, + cols: range(from, to), + hidden: 0 + }); + }; + + /** + * create group for rows + * @param {Number} from + * @param {Number} to + */ + var groupRows = function (from, to) { + var rangeGroups = getRangeGroups("rows", from, to), + lastLevel = getLastLevelRowsInRange(rangeGroups.groups); + + levels.rows = Math.max(levels.rows, lastLevel + 1); + + + if (!counters.rows) { + counters.rows = getRowGroups().length; + } + + counters.rows++; + groups.push({ + id: 'r' + counters.rows, + level: lastLevel + 1, + rows: range(from, to), + hidden: 0 + }); + }; + + /** + * show or hide groups + * @param showHide + * @param groups + */ + var showHideGroups = function (hidden, groups) { + var level; + for (var i = 0, groupsLength = groups.length; i < groupsLength; i++) { + groups[i].hidden = hidden; + level = groups[i].level; + + if (!hiddenRows[level]) { + hiddenRows[level] = []; + } + if (!hiddenCols[level]) { + hiddenCols[level] = []; + } + + if (groups[i].rows) { + for (var j = 0, rowsLength = groups[i].rows.length; j < rowsLength; j++) { + if (hidden > 0) { + hiddenRows[level][groups[i].rows[j]] = true; + } else { + hiddenRows[level][groups[i].rows[j]] = void 0; + } + } + } else if (groups[i].cols) { + for (var j = 0, colsLength = groups[i].cols.length; j < colsLength; j++) { + if (hidden > 0) { + hiddenCols[level][groups[i].cols[j]] = true; + } else { + hiddenCols[level][groups[i].cols[j]] = void 0; + } + } + } + } + }; + + /** + * Check if the next cell of the dimension (row / column) contains a group at the same level + * @param dimension + * @param currentPosition + * @param level + * @param currentGroupId + * @returns {boolean} + */ + var nextIndexSharesLevel = function (dimension, currentPosition, level, currentGroupId) { + var nextCellGroupId + , levelsByOrder; + + switch (dimension) { + case 'rows': + nextCellGroupId = getGroupByRowAndLevel(currentPosition + 1, level).id; + levelsByOrder = Handsontable.Grouping.getGroupLevelsByRows(); + break; + case 'cols': + nextCellGroupId = getGroupByColAndLevel(currentPosition + 1, level).id; + levelsByOrder = Handsontable.Grouping.getGroupLevelsByCols(); + break; + } + + return !!(levelsByOrder[currentPosition + 1] && levelsByOrder[currentPosition + 1].indexOf(level) > -1 && currentGroupId == nextCellGroupId); + + }; + + /** + * Check if the previous cell of the dimension (row / column) contains a group at the same level + * @param dimension + * @param currentPosition + * @param level + * @param currentGroupId + * @returns {boolean} + */ + var previousIndexSharesLevel = function (dimension, currentPosition, level, currentGroupId) { + var previousCellGroupId + , levelsByOrder; + + switch (dimension) { + case 'rows': + previousCellGroupId = getGroupByRowAndLevel(currentPosition - 1, level).id; + levelsByOrder = Handsontable.Grouping.getGroupLevelsByRows(); + break; + case 'cols': + previousCellGroupId = getGroupByColAndLevel(currentPosition - 1, level).id; + levelsByOrder = Handsontable.Grouping.getGroupLevelsByCols(); + break; + } + + return !!(levelsByOrder[currentPosition - 1] && levelsByOrder[currentPosition - 1].indexOf(level) > -1 && currentGroupId == previousCellGroupId); + + }; + + /** + * Check if the provided index is at the end of the group indicator line + * @param dimension + * @param index + * @param level + * @param currentGroupId + * @returns {boolean} + */ + var isLastIndexOfTheLine = function (dimension, index, level, currentGroupId) { + if (index === 0) { + return false; + } + var levelsByOrder + , entriesLength + , previousSharesLevel = previousIndexSharesLevel(dimension, index, level, currentGroupId) + , nextSharesLevel = nextIndexSharesLevel(dimension, index, level, currentGroupId) + , nextIsHidden = false; + + switch (dimension) { + case 'rows': + levelsByOrder = Handsontable.Grouping.getGroupLevelsByRows(); + entriesLength = instance.countRows(); + for (var i = 0; i <= levels.rows; i++) { + if (hiddenRows[i] && hiddenRows[i][index + 1]) { + nextIsHidden = true; + break; + } + } + break; + case 'cols': + levelsByOrder = Handsontable.Grouping.getGroupLevelsByCols(); + entriesLength = instance.countCols(); + for (var i = 0; i <= levels.cols; i++) { + if (hiddenCols[i] && hiddenCols[i][index + 1]) { + nextIsHidden = true; + break; + } + } + break; + } + + if (previousSharesLevel) { + if (index == entriesLength - 1) { + return true; + } else if (!nextSharesLevel || (nextSharesLevel && nextIsHidden)) { + return true; + } else if (!levelsByOrder[index + 1]) { + return true; + } + } + return false; + }; + + /** + * Check if all rows/cols are hidden + * @param dataType + */ + var isLastHidden = function (dataType) { + var levelAmount; + + switch (dataType) { + case 'rows': + levelAmount = levels.rows; + for (var j = 0; j <= levelAmount; j++) { + if (hiddenRows[j] && hiddenRows[j][instance.countRows() - 1]) { + return true; + } + } + + break; + case 'cols': + levelAmount = levels.cols; + for (var j = 0; j <= levelAmount; j++) { + if (hiddenCols[j] && hiddenCols[j][instance.countCols() - 1]) { + return true; + } + } + break; + } + + return false; + }; + + /** + * Check if the provided index is at the beginning of the group indicator line + * @param dimension + * @param index + * @param level + * @param currentGroupId + * @returns {boolean} + */ + var isFirstIndexOfTheLine = function (dimension, index, level, currentGroupId) { + var levelsByOrder + , entriesLength + , currentGroup = getGroupById(currentGroupId) + , previousAreHidden = false + , arePreviousHidden = function (dimension) { + var hidden = false + , hiddenArr = dimension == 'rows' ? hiddenRows : hiddenCols; + for (var i = 0; i <= levels[dimension]; i++) { + tempInd = index; + while (currentGroup[dimension].indexOf(tempInd) > -1) { + hidden = !!(hiddenArr[i] && hiddenArr[i][tempInd]); + tempInd--; + } + if (hidden) { + break; + } + } + return hidden; + } + , previousSharesLevel = previousIndexSharesLevel(dimension, index, level, currentGroupId) + , nextSharesLevel = nextIndexSharesLevel(dimension, index, level, currentGroupId) + , tempInd; + + switch (dimension) { + case 'rows': + levelsByOrder = Handsontable.Grouping.getGroupLevelsByRows(); + entriesLength = instance.countRows(); + previousAreHidden = arePreviousHidden(dimension); + break; + case 'cols': + levelsByOrder = Handsontable.Grouping.getGroupLevelsByCols(); + entriesLength = instance.countCols(); + previousAreHidden = arePreviousHidden(dimension); + break; + } + + if (index == entriesLength - 1) { + return false; + } + else if (index === 0) { + if (nextSharesLevel) { + return true; + } + } else if (!previousSharesLevel || (previousSharesLevel && previousAreHidden)) { + if (nextSharesLevel) { + return true; + } + } else if (!levelsByOrder[index - 1]) { + if (nextSharesLevel) { + return true; + } + } + return false; + }; + + /** + * Add group expander button + * @param dimension + * @param index + * @param level + * @param id + * @param elem + * @returns {*} + */ + var addGroupExpander = function (dataType, index, level, id, elem) { + var previousIndexGroupId; + + switch (dataType) { + case 'rows': + previousIndexGroupId = getGroupByRowAndLevel(index - 1, level).id; + break; + case 'cols': + previousIndexGroupId = getGroupByColAndLevel(index - 1, level).id; + break; + } + + if (!previousIndexGroupId) { + return null; + } + + if (index > 0) { + if (previousIndexSharesLevel(dataType, index - 1, level, previousIndexGroupId) && previousIndexGroupId != id) { + + var expanderButton = document.createElement('DIV'); + Handsontable.Dom.addClass(expanderButton, classes.expandButton); + expanderButton.id = 'htExpand-' + previousIndexGroupId; + expanderButton.appendChild(document.createTextNode('+')); + expanderButton.setAttribute('data-level', level); + expanderButton.setAttribute('data-type', dataType); + expanderButton.setAttribute('data-hidden', "1"); + + elem.appendChild(expanderButton); + + return expanderButton; + } + } + return null; + }; + + /** + * Check if provided cell is collapsed (either by rows or cols) + * @param currentPosition + * @returns {boolean} + */ + var isCollapsed = function (currentPosition) { + var rowGroups = getRowGroups() + , colGroups = getColGroups(); + + for (var i = 0, rowGroupsCount = rowGroups.length; i < rowGroupsCount; i++) { + if (rowGroups[i].rows.indexOf(currentPosition.row) > -1 && rowGroups[i].hidden) { + return true; + } + } + + if (currentPosition.col === null) { // if col is set to null, check only rows + return false; + } + + for (var i = 0, colGroupsCount = colGroups.length; i < colGroupsCount; i++) { + if (colGroups[i].cols.indexOf(currentPosition.col) > -1 && colGroups[i].hidden) { + return true; + } + } + + return false; + }; + + return { + + /** + * all groups for ht instance + */ + getGroups: function () { + return groups; + }, + /** + * All levels for rows and cols respectively + */ + getLevels: function () { + return levels; + }, + /** + * Current instance + */ + instance: instance, + /** + * Initial setting for minSpareRows + */ + baseSpareRows: instance.getSettings().minSpareRows, + /** + * Initial setting for minSpareCols + */ + baseSpareCols: instance.getSettings().minSpareCols, + + getRowGroups: getRowGroups, + getColGroups: getColGroups, + /** + * init group + * @param {Object} settings, could be an array of objects [{cols: [0,1,2]}, {cols: [3,4,5]}, {rows: [0,1]}] + */ + init: function () { + var groupsSetting = instance.getSettings().groups; + if (groupsSetting) { + if (Array.isArray(groupsSetting)) { + Handsontable.Grouping.initGroups(groupsSetting); + } + } + }, + + /** + * init groups from configuration on startup + */ + initGroups: function (initialGroups) { + var that = this; + + groups = []; + + initialGroups.forEach(function (item) { + var _group = [], + isRow = false, + isCol = false; + + if (Array.isArray(item.rows)) { + _group = item.rows; + isRow = true; + } else if (Array.isArray(item.cols)) { + _group = item.cols; + isCol = true; + } + + var from = _group[0], + to = _group[_group.length - 1]; + + if (isRow) { + groupRows(from, to); + } else if (isCol) { + groupCols(from, to); + } + }); +// this.render(); + }, + + /** + * Remove all existing groups + */ + resetGroups: function () { + groups = []; + counters = { + rows: 0, + cols: 0 + }; + levels = { + rows: 0, + cols: 0 + }; + + var allOccurrences; + for (var i in classes) { + if (typeof classes[i] != 'function') { + allOccurrences = document.querySelectorAll('.' + classes[i]); + for (var j = 0, occurrencesLength = allOccurrences.length; j < occurrencesLength; j++) { + Handsontable.Dom.removeClass(allOccurrences[j], classes[i]); + } + } + } + + var otherClasses = ['htGroupColClosest', 'htGroupCol']; + for (var i = 0, otherClassesLength = otherClasses.length; i < otherClassesLength; i++) { + allOccurrences = document.querySelectorAll('.' + otherClasses[i]); + for (var j = 0, occurrencesLength = allOccurrences.length; j < occurrencesLength; j++) { + Handsontable.Dom.removeClass(allOccurrences[j], otherClasses[i]); + } + } + }, + /** + * Update groups from the instance settings + */ + updateGroups: function () { + var groupSettings = this.getSettings().groups; + + Handsontable.Grouping.resetGroups(); + Handsontable.Grouping.initGroups(groupSettings); + }, + afterGetRowHeader: function (row, TH) { + var currentRowHidden = false; + for (var i = 0, levels = hiddenRows.length; i < levels; i++) { + if (hiddenRows[i] && hiddenRows[i][row] === true) { + currentRowHidden = true; + } + } + + if (currentRowHidden) { + Handsontable.Dom.addClass(TH.parentNode, 'hidden'); + } else if (!currentRowHidden && Handsontable.Dom.hasClass(TH.parentNode, 'hidden')) { + Handsontable.Dom.removeClass(TH.parentNode, 'hidden'); + } + + }, + afterGetColHeader: function (col, TH) { + var rowHeaders = this.view.wt.wtSettings.getSetting('rowHeaders').length + , thisColgroup = instance.rootElement.querySelectorAll('colgroup col:nth-child(' + parseInt(col + rowHeaders + 1, 10) + ')'); + + if (thisColgroup.length === 0) { + return; + } + + var currentColHidden = false; + for (var i = 0, levels = hiddenCols.length; i < levels; i++) { + if (hiddenCols[i] && hiddenCols[i][col] === true) { + currentColHidden = true; + } + } + + if (currentColHidden) { + for (var i = 0, colsAmount = thisColgroup.length; i < colsAmount; i++) { + Handsontable.Dom.addClass(thisColgroup[i], 'hidden'); + } + } else if (!currentColHidden && Handsontable.Dom.hasClass(thisColgroup[0], 'hidden')) { + for (var i = 0, colsAmount = thisColgroup.length; i < colsAmount; i++) { + Handsontable.Dom.removeClass(thisColgroup[i], 'hidden'); + } + } + }, + /** + * Create a renderer for additional row/col headers, acting as group indicators + * @param walkontableConfig + * @param direction + */ + groupIndicatorsFactory: function (renderersArr, direction) { + var groupsLevelsList + , getCurrentLevel + , getCurrentGroupId + , dataType + , getGroupByIndexAndLevel + , headersType + , currentHeaderModifier + , createLevelTriggers; + + switch (direction) { + case 'horizontal': + groupsLevelsList = Handsontable.Grouping.getGroupLevelsByCols(); + getCurrentLevel = function (elem) { + return Array.prototype.indexOf.call(elem.parentNode.parentNode.childNodes, elem.parentNode) + 1; + }; + getCurrentGroupId = function (col, level) { + return getGroupByColAndLevel(col, level).id; + }; + dataType = 'cols'; + getGroupByIndexAndLevel = function (col, level) { + return getGroupByColAndLevel(col - 1, level); + }; + headersType = "columnHeaders"; + currentHeaderModifier = function (headerRenderers) { + if (headerRenderers.length === 1) { + var oldFn = headerRenderers[0]; + + headerRenderers[0] = function (index, elem, level) { + + if (index < -1) { + makeGroupIndicatorsForLevel()(index, elem, level); + } else { + Handsontable.Dom.removeClass(elem, classes.groupIndicatorContainer); + oldFn(index, elem, level); + } + }; + } + return function () { + return headerRenderers; + }; + }; + createLevelTriggers = true; + break; + case 'vertical': + groupsLevelsList = Handsontable.Grouping.getGroupLevelsByRows(); + getCurrentLevel = function (elem) { + return Handsontable.Dom.index(elem) + 1; + }; + getCurrentGroupId = function (row, level) { + return getGroupByRowAndLevel(row, level).id; + }; + dataType = 'rows'; + getGroupByIndexAndLevel = function (row, level) { + return getGroupByRowAndLevel(row - 1, level); + }; + headersType = "rowHeaders"; + currentHeaderModifier = function (headerRenderers) { + return headerRenderers; + }; + break; + } + + var createButton = function (parent) { + var button = document.createElement('div'); + + parent.appendChild(button); + + return { + button: button, + addClass: function (className) { + Handsontable.Dom.addClass(button, className); + } + }; + }; + + var makeGroupIndicatorsForLevel = function () { + var directionClassname = direction.charAt(0).toUpperCase() + direction.slice(1); // capitalize the first letter + + return function (index, elem, level) { // header rendering function + + level++; + var child + , collapseButton; + + /* jshint -W084 */ + while (child = elem.lastChild) { + elem.removeChild(child); + } + + Handsontable.Dom.addClass(elem, classes.groupIndicatorContainer); + + var currentGroupId = getCurrentGroupId(index, level); + + if (index > -1 && (groupsLevelsList[index] && groupsLevelsList[index].indexOf(level) > -1)) { + + collapseButton = createButton(elem); + collapseButton.addClass(classes.groupIndicator(directionClassname)); + + if (isFirstIndexOfTheLine(dataType, index, level, currentGroupId)) { // add a little thingy and the top of the group indicator + collapseButton.addClass(classes.groupStart); + } + + if (isLastIndexOfTheLine(dataType, index, level, currentGroupId)) { // add [+]/[-] button at the end of the line + collapseButton.button.appendChild(document.createTextNode('-')); + collapseButton.addClass(classes.collapseButton); + collapseButton.button.id = classes.collapseGroupId(currentGroupId); + collapseButton.button.setAttribute('data-level', level); + collapseButton.button.setAttribute('data-type', dataType); + } + + } + + if (createLevelTriggers) { + var rowInd = Handsontable.Dom.index(elem.parentNode); + if (index === -1 || (index < -1 && rowInd === Handsontable.Grouping.getLevels().cols + 1) || + (rowInd === 0 && Handsontable.Grouping.getLevels().cols === 0)) { + collapseButton = createButton(elem); + collapseButton.addClass(classes.levelTrigger); + + if (index === -1) { + collapseButton.button.id = classes.collapseFromLevel("Cols", level); + collapseButton.button.appendChild(document.createTextNode(level)); + } else if (index < -1 && rowInd === Handsontable.Grouping.getLevels().cols + 1 || + (rowInd === 0 && Handsontable.Grouping.getLevels().cols === 0)) { + var colInd = Handsontable.Dom.index(elem) + 1; + collapseButton.button.id = classes.collapseFromLevel("Rows", colInd); + collapseButton.button.appendChild(document.createTextNode(colInd)); + } + } + } + + // add group expending button + var expanderButton = addGroupExpander(dataType, index, level, currentGroupId, elem); + if (index > 0) { + var previousGroupObj = getGroupByIndexAndLevel(index - 1, level); + + if (expanderButton && previousGroupObj.hidden) { + Handsontable.Dom.addClass(expanderButton, classes.clickable); + } + } + + updateHeaderWidths(); + + }; + }; + + + renderersArr = currentHeaderModifier(renderersArr); + + + if (counters[dataType] > 0) { + for (var i = 0; i < levels[dataType] + 1; i++) { // for each level of col groups add a header renderer + if (!Array.isArray(renderersArr)) { + renderersArr = typeof renderersArr === 'function' ? renderersArr() : new Array(renderersArr); + } + renderersArr.unshift(makeGroupIndicatorsForLevel()); + } + } + }, + /** + * Get group levels array arranged by rows + * @returns {Array} + */ + getGroupLevelsByRows: function () { + var rowGroups = getRowGroups() + , result = []; + + for (var i = 0, groupsLength = rowGroups.length; i < groupsLength; i++) { + if (rowGroups[i].rows) { + for (var j = 0, groupRowsLength = rowGroups[i].rows.length; j < groupRowsLength; j++) { + if (!result[rowGroups[i].rows[j]]) { + result[rowGroups[i].rows[j]] = []; + } + result[rowGroups[i].rows[j]].push(rowGroups[i].level); + } + } + } + return result; + }, + /** + * Get group levels array arranged by cols + * @returns {Array} + */ + getGroupLevelsByCols: function () { + var colGroups = getColGroups() + , result = []; + + for (var i = 0, groupsLength = colGroups.length; i < groupsLength; i++) { + if (colGroups[i].cols) { + for (var j = 0, groupColsLength = colGroups[i].cols.length; j < groupColsLength; j++) { + if (!result[colGroups[i].cols[j]]) { + result[colGroups[i].cols[j]] = []; + } + result[colGroups[i].cols[j]].push(colGroups[i].level); + } + } + } + return result; + }, + /** + * Toggle the group visibility ( + / - event handler) + * @param event + * @param coords + * @param TD + */ + toggleGroupVisibility: function (event, coords, TD) { + if (Handsontable.Dom.hasClass(event.target, classes.expandButton) || + Handsontable.Dom.hasClass(event.target, classes.collapseButton) || + Handsontable.Dom.hasClass(event.target, classes.levelTrigger)) { + var element = event.target + , elemIdSplit = element.id.split('-'); + + var groups = [] + , id + , level + , type + , hidden; + + var prepareGroupData = function (componentElement) { + if (componentElement) { + element = componentElement; + } + + elemIdSplit = element.id.split('-'); + + id = elemIdSplit[1]; + level = parseInt(element.getAttribute('data-level'), 10); + type = element.getAttribute('data-type'); + hidden = parseInt(element.getAttribute('data-hidden')); + + if (isNaN(hidden)) { + hidden = 1; + } else { + hidden = (hidden ? 0 : 1); + } + + element.setAttribute('data-hidden', hidden.toString()); + + + groups.push(getGroupById(id)); + }; + + if (element.className.indexOf(classes.levelTrigger) > -1) { // show levels below, hide all above + var groupsInLevel + , groupsToExpand = [] + , groupsToCollapse = [] + , levelType = element.id.indexOf("Rows") > -1 ? "rows" : "cols"; + + for (var i = 1, levelsCount = levels[levelType]; i <= levelsCount; i++) { + groupsInLevel = levelType == "rows" ? getRowGroupsByLevel(i) : getColGroupsByLevel(i); + + if (i >= parseInt(elemIdSplit[1], 10)) { + for (var j = 0, groupCount = groupsInLevel.length; j < groupCount; j++) { + groupsToCollapse.push(groupsInLevel[j]); + } + } else { + for (var j = 0, groupCount = groupsInLevel.length; j < groupCount; j++) { + groupsToExpand.push(groupsInLevel[j]); + } + } + } + + showHideGroups(true, groupsToCollapse); + showHideGroups(false, groupsToExpand); + + } else { + prepareGroupData(); + showHideGroups(hidden, groups); + } + + // add the expander button to a dummy spare row/col, if no longer needed -> remove it + /* jshint -W038 */ + type = type || levelType; + + var lastHidden = isLastHidden(type) + , typeUppercase = type.charAt(0).toUpperCase() + type.slice(1) + , spareElements = Handsontable.Grouping['baseSpare' + typeUppercase]; + + if (lastHidden) { + /* jshint -W041 */ + if (spareElements == 0) { + instance.alter('insert_' + type.slice(0, -1), instance['count' + typeUppercase]()); + Handsontable.Grouping["dummy" + type.slice(0, -1)] = true; + } + } else { + /* jshint -W041 */ + if (spareElements == 0) { + if (Handsontable.Grouping["dummy" + type.slice(0, -1)]) { + instance.alter('remove_' + type.slice(0, -1), instance['count' + typeUppercase]() - 1); + Handsontable.Grouping["dummy" + type.slice(0, -1)] = false; + } + } + } + + instance.render(); + + event.stopImmediatePropagation(); + } + }, + /** + * Modify the delta when changing cells using keyobard + * @param position + * @returns {Function} + */ + modifySelectionFactory: function (position) { + var instance = this.instance; + var currentlySelected + , nextPosition = new WalkontableCellCoords(0, 0) + , nextVisible = function (direction, currentPosition) { // updates delta to skip to the next visible cell + var updateDelta = 0; + + switch (direction) { + case 'down': + while (isCollapsed(currentPosition)) { + updateDelta++; + currentPosition.row += 1; + } + break; + case 'up': + while (isCollapsed(currentPosition)) { + updateDelta--; + currentPosition.row -= 1; + } + break; + case 'right': + while (isCollapsed(currentPosition)) { + updateDelta++; + currentPosition.col += 1; + } + break; + case 'left': + while (isCollapsed(currentPosition)) { + updateDelta--; + currentPosition.col -= 1; + } + break; + } + + return updateDelta; + } + , updateDelta = function (delta, nextPosition) { + if (delta.row > 0) { // moving down + if (isCollapsed(nextPosition)) { + delta.row += nextVisible('down', nextPosition); + } + } else if (delta.row < 0) { // moving up + if (isCollapsed(nextPosition)) { + delta.row += nextVisible('up', nextPosition); + } + } + + if (delta.col > 0) { // moving right + if (isCollapsed(nextPosition)) { + delta.col += nextVisible('right', nextPosition); + } + } else if (delta.col < 0) { // moving left + if (isCollapsed(nextPosition)) { + delta.col += nextVisible('left', nextPosition); + } + } + }; + + /* jshint -W027 */ + switch (position) { + case 'start': + return function (delta) { + currentlySelected = instance.getSelected(); + nextPosition.row = currentlySelected[0] + delta.row; + nextPosition.col = currentlySelected[1] + delta.col; + + updateDelta(delta, nextPosition); + }; + break; + case 'end': + return function (delta) { + currentlySelected = instance.getSelected(); + nextPosition.row = currentlySelected[2] + delta.row; + nextPosition.col = currentlySelected[3] + delta.col; + + updateDelta(delta, nextPosition); + }; + break; + } + }, + modifyRowHeight: function (height, row) { + if (instance.view.wt.wtTable.rowFilter && isCollapsed({row: row, col: null})) { + return 0; + } + }, + validateGroups: function () { + + var areRangesOverlapping = function (a, b) { + if ((a[0] < b[0] && a[1] < b[1] && b[0] <= a[1]) || + (a[0] > b[0] && b[1] < a[1] && a[0] <= b[1])) { + return true; + } + }; + + var configGroups = instance.getSettings().groups + , cols = [] + , rows = []; + + for (var i = 0, groupsLength = configGroups.length; i < groupsLength; i++) { + if (configGroups[i].rows) { + /* jshint -W027 */ + if(configGroups[i].rows.length === 1) { // single-entry group + throw new Error("Grouping error: Group {" + configGroups[i].rows[0] + "} is invalid. Cannot define single-entry groups."); + return false; + } else if(configGroups[i].rows.length === 0) { + throw new Error("Grouping error: Cannot define empty groups."); + return false; + } + + rows.push(configGroups[i].rows); + + for (var j = 0, rowsLength = rows.length; j < rowsLength; j++) { + if (areRangesOverlapping(configGroups[i].rows, rows[j])) { + + throw new Error("Grouping error: ranges {" + configGroups[i].rows[0] + ", " + configGroups[i].rows[1] + "} and {" + rows[j][0] + ", " + rows[j][1] + "} are overlapping."); + return false; + } + } + } else if (configGroups[i].cols) { + + if(configGroups[i].cols.length === 1) { // single-entry group + throw new Error("Grouping error: Group {" + configGroups[i].cols[0] + "} is invalid. Cannot define single-entry groups."); + return false; + } else if(configGroups[i].cols.length === 0) { + throw new Error("Grouping error: Cannot define empty groups."); + return false; + } + + cols.push(configGroups[i].cols); + + for (var j = 0, colsLength = cols.length; j < colsLength; j++) { + if (areRangesOverlapping(configGroups[i].cols, cols[j])) { + + throw new Error("Grouping error: ranges {" + configGroups[i].cols[0] + ", " + configGroups[i].cols[1] + "} and {" + cols[j][0] + ", " + cols[j][1] + "} are overlapping."); + return false; + } + } + } + } + + return true; + }, + afterGetRowHeaderRenderers: function (arr) { + Handsontable.Grouping.groupIndicatorsFactory(arr, 'vertical'); + }, + afterGetColumnHeaderRenderers: function (arr) { + Handsontable.Grouping.groupIndicatorsFactory(arr, 'horizontal'); + }, + hookProxy: function (fn, arg) { + return function () { + if (instance.getSettings().groups) { + return arg ? Handsontable.Grouping[fn](arg).apply(this, arguments) : Handsontable.Grouping[fn].apply(this, arguments); + } else { + return void 0; + } + }; + } + }; +}; + +/** + * create new instance + */ +var init = function () { + var instance = this, + groupingSetting = !!(instance.getSettings().groups); + + + if (groupingSetting) { + var headerUpdates = {}; + + Handsontable.Grouping = new Grouping(instance); + + if (!instance.getSettings().rowHeaders) { // force using rowHeaders -- needs to be changed later + headerUpdates.rowHeaders = true; + } + if (!instance.getSettings().colHeaders) { // force using colHeaders -- needs to be changed later + headerUpdates.colHeaders = true; + } + if (headerUpdates.colHeaders || headerUpdates.rowHeaders) { + instance.updateSettings(headerUpdates); + } + + var groupConfigValid = Handsontable.Grouping.validateGroups(); + if (!groupConfigValid) { + return; + } + + instance.addHook('beforeInit', Handsontable.Grouping.hookProxy('init')); + instance.addHook('afterUpdateSettings', Handsontable.Grouping.hookProxy('updateGroups')); + instance.addHook('afterGetColumnHeaderRenderers', Handsontable.Grouping.hookProxy('afterGetColumnHeaderRenderers')); + instance.addHook('afterGetRowHeaderRenderers', Handsontable.Grouping.hookProxy('afterGetRowHeaderRenderers')); + instance.addHook('afterGetRowHeader', Handsontable.Grouping.hookProxy('afterGetRowHeader')); + instance.addHook('afterGetColHeader', Handsontable.Grouping.hookProxy('afterGetColHeader')); + instance.addHook('beforeOnCellMouseDown', Handsontable.Grouping.hookProxy('toggleGroupVisibility')); + instance.addHook('modifyTransformStart', Handsontable.Grouping.hookProxy('modifySelectionFactory', 'start')); + instance.addHook('modifyTransformEnd', Handsontable.Grouping.hookProxy('modifySelectionFactory', 'end')); + instance.addHook('modifyRowHeight', Handsontable.Grouping.hookProxy('modifyRowHeight')); + } +}; + +/** + * Update headers widths for the group indicators + */ +// TODO: this needs cleaning up +var updateHeaderWidths = function () { + var colgroups = document.querySelectorAll('colgroup'); + for (var i = 0, colgroupsLength = colgroups.length; i < colgroupsLength; i++) { + var rowHeaders = colgroups[i].querySelectorAll('col.rowHeader'); + if (rowHeaders.length === 0) { + return; + } + for (var j = 0, rowHeadersLength = rowHeaders.length + 1; j < rowHeadersLength; j++) { + if (rowHeadersLength == 2) { + return; + } + if (j < Handsontable.Grouping.getLevels().rows + 1) { + if (j == Handsontable.Grouping.getLevels().rows) { + Handsontable.Dom.addClass(rowHeaders[j], 'htGroupColClosest'); + } else { + Handsontable.Dom.addClass(rowHeaders[j], 'htGroupCol'); + } + } + } + } +}; + +Handsontable.hooks.add('beforeInit', init); + +Handsontable.hooks.add('afterUpdateSettings', function () { + + if (this.getSettings().groups && !Handsontable.Grouping) { + init.call(this, arguments); + } else if (!this.getSettings().groups && Handsontable.Grouping) { + Handsontable.Grouping.resetGroups(); + Handsontable.Grouping = void 0; + } +}); + +Handsontable.plugins.Grouping = Grouping; + +(function (Handsontable) { + /** + * Plugin used to allow user to copy and paste from the context menu + * Currently uses ZeroClipboard due to browser limitations + * @constructor + */ + function ContextMenuCopyPaste() { + this.zeroClipboardInstance = null; + this.instance = null; + } + + /** + * Configure ZeroClipboard + */ + ContextMenuCopyPaste.prototype.prepareZeroClipboard = function () { + if(this.swfPath) { + ZeroClipboard.config({ + swfPath: this.swfPath + }); + } + }; + + /** + * Copy action + * @returns {CopyPasteClass.elTextarea.value|*} + */ + ContextMenuCopyPaste.prototype.copy = function () { + this.instance.copyPaste.setCopyableText(); + return this.instance.copyPaste.copyPasteInstance.elTextarea.value; + }; + + /** + * Adds copy/paste items to context menu + */ + ContextMenuCopyPaste.prototype.addToContextMenu = function (defaultOptions) { + if (!this.getSettings().contextMenuCopyPaste) { + return; + } + + defaultOptions.items.unshift( + { + key: 'copy', + name: 'Copy' + }, + { + key: 'paste', + name: 'Paste', + callback: function () { + this.copyPaste.triggerPaste(); + } + }, + Handsontable.ContextMenu.SEPARATOR + ); + }; + + /** + * Setup ZeroClipboard swf clip position and event handlers + * @param cmInstance Current context menu instance + */ + ContextMenuCopyPaste.prototype.setupZeroClipboard = function (cmInstance) { + var plugin = this; + this.cmInstance = cmInstance; + + if (!Handsontable.Dom.hasClass(this.cmInstance.rootElement, 'htContextMenu')) { + return; + } + + var data = cmInstance.getData(); + for (var i = 0, ilen = data.length; i < ilen; i++) { //find position of 'copy' option + /*jshint -W083 */ + if (data[i].key === 'copy') { + this.zeroClipboardInstance = new ZeroClipboard(cmInstance.getCell(i, 0)); + + this.zeroClipboardInstance.off(); + this.zeroClipboardInstance.on("copy", function (event) { + var clipboard = event.clipboardData; + clipboard.setData("text/plain", plugin.copy()); + plugin.instance.getSettings().outsideClickDeselects = plugin.outsideClickDeselectsCache; + }); + + cmCopyPaste.bindEvents(); + break; + } + } + }; + + /** + * Bind all the standard events + */ + ContextMenuCopyPaste.prototype.bindEvents = function () { + var plugin = this; + + // Workaround for 'current' and 'zeroclipboard-is-hover' classes being stuck when moving the cursor over the context menu + if (plugin.cmInstance) { + + var eventManager = new Handsontable.eventManager(this.instance); + + var removeCurrenClass = function (event) { + var hadClass = plugin.cmInstance.rootElement.querySelector('td.current'); + if (hadClass) { + Handsontable.Dom.removeClass(hadClass, 'current'); + } + plugin.outsideClickDeselectsCache = plugin.instance.getSettings().outsideClickDeselects; + plugin.instance.getSettings().outsideClickDeselects = false; + }; + + var removeZeroClipboardClass = function (event) { + var hadClass = plugin.cmInstance.rootElement.querySelector('td.zeroclipboard-is-hover'); + if (hadClass) { + Handsontable.Dom.removeClass(hadClass, 'zeroclipboard-is-hover'); + } + plugin.instance.getSettings().outsideClickDeselects = plugin.outsideClickDeselectsCache; + }; + + eventManager.removeEventListener(document,'mouseenter', function () { + removeCurrenClass(); + }); + eventManager.addEventListener(document, 'mouseenter', function (e) { + removeCurrenClass(); + }); + + eventManager.removeEventListener(document,'mouseleave', function () { + removeZeroClipboardClass(); + }); + eventManager.addEventListener(document, 'mouseleave', function (e) { + removeZeroClipboardClass(); + }); + + + } + }; + + /** + * Initialize plugin + * @returns {boolean} Returns false if ZeroClipboard is not properly included + */ + ContextMenuCopyPaste.prototype.init = function () { + if (!this.getSettings().contextMenuCopyPaste) { + return; + } else if (typeof this.getSettings().contextMenuCopyPaste == "object") { + cmCopyPaste.swfPath = this.getSettings().contextMenuCopyPaste.swfPath; + } + + /* jshint ignore:start */ + if (typeof ZeroClipboard === 'undefined') { + throw new Error("To be able to use the Copy/Paste feature from the context menu, you need to manualy include ZeroClipboard.js file to your website."); + + return false; + } + try { + var flashTest = new ActiveXObject('ShockwaveFlash.ShockwaveFlash'); + } catch(exception) { + if (!('undefined' != typeof navigator.mimeTypes['application/x-shockwave-flash'])) { + throw new Error("To be able to use the Copy/Paste feature from the context menu, your browser needs to have Flash Plugin installed."); + + return false; + } + } + /* jshint ignore:end */ + + cmCopyPaste.instance = this; + cmCopyPaste.prepareZeroClipboard(); + }; + + var cmCopyPaste = new ContextMenuCopyPaste(); + + Handsontable.hooks.add('afterRender', function () { + cmCopyPaste.setupZeroClipboard(this); + }); + + Handsontable.hooks.add('afterInit', cmCopyPaste.init); + Handsontable.hooks.add('afterContextMenuDefaultOptions', cmCopyPaste.addToContextMenu); + Handsontable.ContextMenuCopyPaste = ContextMenuCopyPaste; + +})(Handsontable); + +(function (Handsontable) { + 'use strict'; + + function MultipleSelectionHandles(instance) { + this.instance = instance; + this.dragged = []; + + this.eventManager = Handsontable.eventManager(instance); + + this.bindTouchEvents(); + } + + MultipleSelectionHandles.prototype.getCurrentRangeCoords = function (selectedRange, currentTouch, touchStartDirection, currentDirection, draggedHandle) { + var topLeftCorner = selectedRange.getTopLeftCorner() + , bottomRightCorner = selectedRange.getBottomRightCorner() + , bottomLeftCorner = selectedRange.getBottomLeftCorner() + , topRightCorner = selectedRange.getTopRightCorner(); + + var newCoords = { + start: null, + end: null + }; + + switch (touchStartDirection) { + case "NE-SW": + switch (currentDirection) { + case "NE-SW": + case "NW-SE": + if (draggedHandle == "topLeft") { + newCoords = { + start: new WalkontableCellCoords(currentTouch.row, selectedRange.highlight.col), + end: new WalkontableCellCoords(bottomLeftCorner.row, currentTouch.col) + }; + } else { + newCoords = { + start: new WalkontableCellCoords(selectedRange.highlight.row, currentTouch.col), + end: new WalkontableCellCoords(currentTouch.row, topLeftCorner.col) + }; + } + break; + case "SE-NW": + if (draggedHandle == "bottomRight") { + newCoords = { + start: new WalkontableCellCoords(bottomRightCorner.row, currentTouch.col), + end: new WalkontableCellCoords(currentTouch.row, topLeftCorner.col) + }; + } + break; + //case "SW-NE": + // break; + } + break; + case "NW-SE": + switch (currentDirection) { + case "NE-SW": + if (draggedHandle == "topLeft") { + newCoords = { + start: currentTouch, + end: bottomLeftCorner + }; + } else { + newCoords.end = currentTouch; + } + break; + case "NW-SE": + if (draggedHandle == "topLeft") { + newCoords = { + start: currentTouch, + end: bottomRightCorner + }; + } else { + newCoords.end = currentTouch; + } + break; + case "SE-NW": + if (draggedHandle == "topLeft") { + newCoords = { + start: currentTouch, + end: topLeftCorner + }; + } else { + newCoords.end = currentTouch; + } + break; + case "SW-NE": + if (draggedHandle == "topLeft") { + newCoords = { + start: currentTouch, + end: topRightCorner + }; + } else { + newCoords.end = currentTouch; + } + break; + } + break; + case "SW-NE": + switch (currentDirection) { + case "NW-SE": + if (draggedHandle == "bottomRight") { + newCoords = { + start: new WalkontableCellCoords(currentTouch.row, topLeftCorner.col), + end: new WalkontableCellCoords(bottomLeftCorner.row, currentTouch.col) + }; + } else { + newCoords = { + start: new WalkontableCellCoords(topLeftCorner.row, currentTouch.col), + end: new WalkontableCellCoords(currentTouch.row, bottomRightCorner.col) + }; + } + break; + //case "NE-SW": + // + // break; + case "SW-NE": + if (draggedHandle == "topLeft") { + newCoords = { + start: new WalkontableCellCoords(selectedRange.highlight.row, currentTouch.col), + end: new WalkontableCellCoords(currentTouch.row, bottomRightCorner.col) + }; + } else { + newCoords = { + start: new WalkontableCellCoords(currentTouch.row, topLeftCorner.col), + end: new WalkontableCellCoords(topLeftCorner.row, currentTouch.col) + }; + } + break; + case "SE-NW": + if (draggedHandle == "bottomRight") { + newCoords = { + start: new WalkontableCellCoords(currentTouch.row, topRightCorner.col), + end: new WalkontableCellCoords(topLeftCorner.row, currentTouch.col) + }; + } else if (draggedHandle == "topLeft") { + newCoords = { + start: bottomLeftCorner, + end: currentTouch + }; + } + break; + } + break; + case "SE-NW": + switch (currentDirection) { + case "NW-SE": + case "NE-SW": + case "SW-NE": + if (draggedHandle == "topLeft") { + newCoords.end = currentTouch; + } + break; + case "SE-NW": + if (draggedHandle == "topLeft") { + newCoords.end = currentTouch; + } else { + newCoords = { + start: currentTouch, + end: topLeftCorner + }; + } + break; + } + break; + } + + return newCoords; + }; + + MultipleSelectionHandles.prototype.bindTouchEvents = function () { + var that = this; + var removeFromDragged = function (query) { + + if (this.dragged.length == 1) { + this.dragged = []; + return true; + } + + var entryPosition = this.dragged.indexOf(query); + + if (entryPosition == -1) { + return false; + } else if (entryPosition === 0) { + this.dragged = this.dragged.slice(0, 1); + } else if (entryPosition == 1) { + this.dragged = this.dragged.slice(-1); + } + }; + + this.eventManager.addEventListener(this.instance.rootElement,'touchstart', function (event) { + if(Handsontable.Dom.hasClass(event.target, "topLeftSelectionHandle-HitArea")) { + that.dragged.push("topLeft"); + var selectedRange = that.instance.getSelectedRange(); + that.touchStartRange = { + width: selectedRange.getWidth(), + height: selectedRange.getHeight(), + direction: selectedRange.getDirection() + }; + event.preventDefault(); + + return false; + } else if (Handsontable.Dom.hasClass(event.target, "bottomRightSelectionHandle-HitArea")) { + that.dragged.push("bottomRight"); + var selectedRange = that.instance.getSelectedRange(); + that.touchStartRange = { + width: selectedRange.getWidth(), + height: selectedRange.getHeight(), + direction: selectedRange.getDirection() + }; + event.preventDefault(); + + return false; + } + }); + + this.eventManager.addEventListener(this.instance.rootElement,'touchend', function (event) { + if(Handsontable.Dom.hasClass(event.target, "topLeftSelectionHandle-HitArea")) { + removeFromDragged.call(that, "topLeft"); + that.touchStartRange = void 0; + event.preventDefault(); + + return false; + } else if (Handsontable.Dom.hasClass(event.target, "bottomRightSelectionHandle-HitArea")) { + removeFromDragged.call(that, "bottomRight"); + that.touchStartRange = void 0; + event.preventDefault(); + + return false; + } + }); + + this.eventManager.addEventListener(this.instance.rootElement,'touchmove', function (event) { + var scrollTop = Handsontable.Dom.getWindowScrollTop() + , scrollLeft = Handsontable.Dom.getWindowScrollLeft(); + + if (that.dragged.length > 0) { + var endTarget = document.elementFromPoint( + event.touches[0].screenX - scrollLeft, + event.touches[0].screenY - scrollTop + ); + + if(!endTarget) { + return; + } + + if (endTarget.nodeName == "TD" || endTarget.nodeName == "TH") { + var targetCoords = that.instance.getCoords(endTarget); + + if(targetCoords.col == -1) { + targetCoords.col = 0; + } + + var selectedRange = that.instance.getSelectedRange() + , rangeWidth = selectedRange.getWidth() + , rangeHeight = selectedRange.getHeight() + , rangeDirection = selectedRange.getDirection(); + + if (rangeWidth == 1 && rangeHeight == 1) { + that.instance.selection.setRangeEnd(targetCoords); + } + + var newRangeCoords = that.getCurrentRangeCoords(selectedRange, targetCoords, that.touchStartRange.direction, rangeDirection, that.dragged[0]); + + if(newRangeCoords.start != null) { + that.instance.selection.setRangeStart(newRangeCoords.start); + } + that.instance.selection.setRangeEnd(newRangeCoords.end); + + } + + event.preventDefault(); + } + }); + + }; + + MultipleSelectionHandles.prototype.isDragged = function () { + if (this.dragged.length === 0) { + return false; + } else { + return true; + } + }; + + var init = function () { + var instance = this; + + Handsontable.plugins.multipleSelectionHandles = new MultipleSelectionHandles(instance); + }; + + Handsontable.hooks.add('afterInit', init); + +})(Handsontable); + +var TouchScroll = (function(instance) { + + function TouchScroll(instance) {} + + TouchScroll.prototype.init = function(instance) { + this.instance = instance; + this.bindEvents(); + + this.scrollbars = [ + this.instance.view.wt.wtScrollbars.vertical, + this.instance.view.wt.wtScrollbars.horizontal, + this.instance.view.wt.wtScrollbars.corner + ]; + + this.clones = [ + this.instance.view.wt.wtScrollbars.vertical.clone.wtTable.holder.parentNode, + this.instance.view.wt.wtScrollbars.horizontal.clone.wtTable.holder.parentNode, + this.instance.view.wt.wtScrollbars.corner.clone.wtTable.holder.parentNode + ]; + }; + + TouchScroll.prototype.bindEvents = function () { + var that = this; + + this.instance.addHook('beforeTouchScroll', function () { + Handsontable.freezeOverlays = true; + + for(var i = 0, cloneCount = that.clones.length; i < cloneCount ; i++) { + Handsontable.Dom.addClass(that.clones[i], 'hide-tween'); + } + }); + + this.instance.addHook('afterMomentumScroll', function () { + Handsontable.freezeOverlays = false; + + for(var i = 0, cloneCount = that.clones.length; i < cloneCount ; i++) { + Handsontable.Dom.removeClass(that.clones[i], 'hide-tween'); + } + + for(var i = 0, cloneCount = that.clones.length; i < cloneCount ; i++) { + Handsontable.Dom.addClass(that.clones[i], 'show-tween'); + } + + setTimeout(function () { + for(var i = 0, cloneCount = that.clones.length; i < cloneCount ; i++) { + Handsontable.Dom.removeClass(that.clones[i], 'show-tween'); + } + },400); + + for(var i = 0, cloneCount = that.scrollbars.length; i < cloneCount ; i++) { + that.scrollbars[i].refresh(); + that.scrollbars[i].resetFixedPosition(); + } + + }); + + }; + + return TouchScroll; +}()); + +var touchScrollHandler = new TouchScroll(); + +Handsontable.hooks.add('afterInit', function() { + touchScrollHandler.init.call(touchScrollHandler, this); +}); + +(function (Handsontable) { + function ManualColumnFreeze(instance) { + var fixedColumnsCount = instance.getSettings().fixedColumnsLeft; + + var init = function () { + // update plugin usages count for manualColumnPositions + if (typeof instance.manualColumnPositionsPluginUsages != 'undefined') { + instance.manualColumnPositionsPluginUsages.push('manualColumnFreeze'); + } else { + instance.manualColumnPositionsPluginUsages = ['manualColumnFreeze']; + } + + bindHooks(); + }; + + /** + * Modifies the default Context Menu entry list to consist 'freeze/unfreeze this column' entries + * @param {Object} defaultOptions + */ + function addContextMenuEntry(defaultOptions) { + defaultOptions.items.push( + Handsontable.ContextMenu.SEPARATOR, + { + key: 'freeze_column', + name: function () { + var selectedColumn = instance.getSelected()[1]; + if (selectedColumn > fixedColumnsCount - 1) { + return 'Freeze this column'; + } else { + return 'Unfreeze this column'; + } + }, + disabled: function () { + var selection = instance.getSelected(); + return selection[1] !== selection[3]; + }, + callback: function () { + var selectedColumn = instance.getSelected()[1]; + if (selectedColumn > fixedColumnsCount - 1) { + freezeColumn(selectedColumn); + } else { + unfreezeColumn(selectedColumn); + } + } + } + ); + } + + /** + * Increments the fixed columns count by one + */ + function addFixedColumn() { + instance.updateSettings({ + fixedColumnsLeft: fixedColumnsCount + 1 + }); + fixedColumnsCount++; + } + + /** + * Decrements the fixed columns count by one + */ + function removeFixedColumn() { + instance.updateSettings({ + fixedColumnsLeft: fixedColumnsCount - 1 + }); + fixedColumnsCount--; + } + + /** + * Checks whether 'manualColumnPositions' array needs creating and/or initializing + * @param {Number} [col] + */ + function checkPositionData(col) { + if (!instance.manualColumnPositions || instance.manualColumnPositions.length === 0) { + if (!instance.manualColumnPositions) { + instance.manualColumnPositions = []; + } + } + if (col) { + if (!instance.manualColumnPositions[col]) { + createPositionData(col + 1); + } + } else { + createPositionData(instance.countCols()); + } + } + + /** + * Fills the 'manualColumnPositions' array with consecutive column indexes + * @param {Number} len + */ + function createPositionData(len) { + if (instance.manualColumnPositions.length < len) { + for (var i = instance.manualColumnPositions.length; i < len; i++) { + instance.manualColumnPositions[i] = i; + } + } + } + + /** + * Updates the column order array used by modifyCol callback + * @param {Number} col + * @param {Number} actualCol column index of the currently selected cell + * @param {Number|null} returnCol suggested return slot for the unfreezed column (can be null) + * @param {String} action 'freeze' or 'unfreeze' + */ + function modifyColumnOrder(col, actualCol, returnCol, action) { + if (returnCol == null) { + returnCol = col; + } + + if (action === 'freeze') { + instance.manualColumnPositions.splice(fixedColumnsCount, 0, instance.manualColumnPositions.splice(actualCol, 1)[0]); + } else if (action === 'unfreeze') { + instance.manualColumnPositions.splice(returnCol, 0, instance.manualColumnPositions.splice(actualCol, 1)[0]); + } + } + + /** + * Estimates the most fitting return position for unfreezed column + * @param {Number} col + */ + function getBestColumnReturnPosition(col) { + var i = fixedColumnsCount, + j = getModifiedColumnIndex(i), + initialCol = getModifiedColumnIndex(col); + while (j < initialCol) { + i++; + j = getModifiedColumnIndex(i); + } + return i - 1; + } + + /** + * Freeze the given column (add it to fixed columns) + * @param {Number} col + */ + function freezeColumn(col) { + if (col <= fixedColumnsCount - 1) { + return; // already fixed + } + + var modifiedColumn = getModifiedColumnIndex(col) || col; + checkPositionData(modifiedColumn); + modifyColumnOrder(modifiedColumn, col, null, 'freeze'); + + addFixedColumn(); + } + + /** + * Unfreeze the given column (remove it from fixed columns and bring to it's previous position) + * @param {Number} col + */ + function unfreezeColumn(col) { + if (col > fixedColumnsCount - 1) { + return; // not fixed + } + + var returnCol = getBestColumnReturnPosition(col); + + var modifiedColumn = getModifiedColumnIndex(col) || col; + checkPositionData(modifiedColumn); + modifyColumnOrder(modifiedColumn, col, returnCol, 'unfreeze'); + removeFixedColumn(); + } + + function getModifiedColumnIndex(col) { + return instance.manualColumnPositions[col]; + } + + /** + * 'modiftyCol' callback + * @param {Number} col + */ + function onModifyCol(col) { + if (this.manualColumnPositionsPluginUsages.length > 1) { // if another plugin is using manualColumnPositions to modify column order, do not double the translation + return col; + } + return getModifiedColumnIndex(col); + } + + function bindHooks() { + //instance.addHook('afterGetColHeader', onAfterGetColHeader); + instance.addHook('modifyCol', onModifyCol); + instance.addHook('afterContextMenuDefaultOptions', addContextMenuEntry); + } + + return { + init: init, + freezeColumn: freezeColumn, + unfreezeColumn: unfreezeColumn, + helpers: { + addFixedColumn: addFixedColumn, + removeFixedColumn: removeFixedColumn, + checkPositionData: checkPositionData, + modifyColumnOrder: modifyColumnOrder, + getBestColumnReturnPosition: getBestColumnReturnPosition + } + }; + } + + var init = function init() { + if (!this.getSettings().manualColumnFreeze) { + return; + } + + var mcfPlugin; + + Handsontable.plugins.manualColumnFreeze = ManualColumnFreeze; + this.manualColumnFreeze = new ManualColumnFreeze(this); + + mcfPlugin = this.manualColumnFreeze; + mcfPlugin.init.call(this); + }; + + Handsontable.hooks.add('beforeInit', init); + +})(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); +}; + +WalkontableOverlay.prototype.makeClone = function (direction) { + var clone = document.createElement('DIV'); + clone.className = 'ht_clone_' + direction + ' handsontable'; + clone.style.position = 'absolute'; + clone.style.top = 0; + clone.style.left = 0; + 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.refresh = function (fastDraw) { + if (this.clone) { + this.clone.draw(fastDraw); + } +}; + +WalkontableOverlay.prototype.destroy = function () { + var eventManager = Handsontable.eventManager(this.clone); + + eventManager.clear(); +}; + +function WalkontableBorder(instance, settings) { + var style; + var createMultipleSelectorHandles = function () { + this.selectionHandles = { + topLeft: document.createElement('DIV'), + topLeftHitArea: document.createElement('DIV'), + bottomRight: document.createElement('DIV'), + bottomRightHitArea: document.createElement('DIV') + }; + var width = 10 + , hitAreaWidth = 40; + + this.selectionHandles.topLeft.className = 'topLeftSelectionHandle'; + this.selectionHandles.topLeftHitArea.className = 'topLeftSelectionHandle-HitArea'; + this.selectionHandles.bottomRight.className = 'bottomRightSelectionHandle'; + this.selectionHandles.bottomRightHitArea.className = 'bottomRightSelectionHandle-HitArea'; + + this.selectionHandles.styles = { + topLeft: this.selectionHandles.topLeft.style, + topLeftHitArea: this.selectionHandles.topLeftHitArea.style, + bottomRight: this.selectionHandles.bottomRight.style, + bottomRightHitArea: this.selectionHandles.bottomRightHitArea.style + }; + + var hitAreaStyle = { + 'position': 'absolute', + 'height': hitAreaWidth + 'px', + 'width': hitAreaWidth + 'px', + 'border-radius': parseInt(hitAreaWidth/1.5,10) + 'px' + }; + + for (var prop in hitAreaStyle) { + if (hitAreaStyle.hasOwnProperty(prop)) { + this.selectionHandles.styles.bottomRightHitArea[prop] = hitAreaStyle[prop]; + this.selectionHandles.styles.topLeftHitArea[prop] = hitAreaStyle[prop]; + } + } + + var handleStyle = { + 'position': 'absolute', + 'height': width + 'px', + 'width': width + 'px', + 'border-radius': parseInt(width/1.5,10) + 'px', + 'background': '#F5F5FF', + 'border': '1px solid #4285c8' + }; + + for (var prop in handleStyle) { + if (handleStyle.hasOwnProperty(prop)) { + this.selectionHandles.styles.bottomRight[prop] = handleStyle[prop]; + this.selectionHandles.styles.topLeft[prop] = handleStyle[prop]; + } + } + + this.main.appendChild(this.selectionHandles.topLeft); + this.main.appendChild(this.selectionHandles.bottomRight); + this.main.appendChild(this.selectionHandles.topLeftHitArea); + this.main.appendChild(this.selectionHandles.bottomRightHitArea); + }; + + if(!settings){ + return; + } + + var eventManager = Handsontable.eventManager(instance); + + //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.cornerDefaultStyle = { + width: '5px', + height: '5px', + borderWidth: '2px', + borderStyle: 'solid', + borderColor: '#FFF' + }; + + this.corner = this.main.childNodes[4]; + this.corner.className += ' corner'; + this.cornerStyle = this.corner.style; + this.cornerStyle.width = this.cornerDefaultStyle.width; + this.cornerStyle.height = this.cornerDefaultStyle.height; + this.cornerStyle.border = [ + this.cornerDefaultStyle.borderWidth, + this.cornerDefaultStyle.borderStyle, + this.cornerDefaultStyle.borderColor + ].join(' '); + + if(Handsontable.mobileBrowser) { + createMultipleSelectorHandles.call(this); + } + + 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; + + + + eventManager.addEventListener(document.body, 'mousedown', function () { + down = true; + }); + + + eventManager.addEventListener(document.body, 'mouseup', function () { + down = false; + }); + + /* jshint ignore:start */ + for (var c = 0, len = this.main.childNodes.length; c < len; c++) { + + eventManager.addEventListener(this.main.childNodes[c], 'mouseenter', function (event) { + if (!down || !instance.getSetting('hideBorderOnMouseDownOver')) { + return; + } + event.preventDefault(); + event.stopImmediatePropagation(); + + var bounds = this.getBoundingClientRect(); + + this.style.display = 'none'; + + 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; + } + }; + + var handler = function (event) { + if (isOutside(event)) { + eventManager.removeEventListener(document.body, 'mousemove', handler); + this.style.display = 'block'; + } + }; + eventManager.addEventListener(document.body, 'mousemove', handler); + }); + } + /* jshint ignore:end */ +} + +/** + * 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; + + var isPartRange = function () { + if(this.instance.selections.area.cellRange) { + + if (toRow != this.instance.selections.area.cellRange.to.row || + toColumn != this.instance.selections.area.cellRange.to.col) { + return true; + } + } + + return false; + }; + + var updateMultipleSelectionHandlesPosition = function (top, left, width, height) { + var handleWidth = parseInt(this.selectionHandles.styles.topLeft.width, 10) + , hitAreaWidth = parseInt(this.selectionHandles.styles.topLeftHitArea.width, 10); + + this.selectionHandles.styles.topLeft.top = parseInt(top - handleWidth,10) + "px"; + this.selectionHandles.styles.topLeft.left = parseInt(left - handleWidth,10) + "px"; + + this.selectionHandles.styles.topLeftHitArea.top = parseInt(top - (hitAreaWidth/4)*3,10) + "px"; + this.selectionHandles.styles.topLeftHitArea.left = parseInt(left - (hitAreaWidth/4)*3,10) + "px"; + + this.selectionHandles.styles.bottomRight.top = parseInt(top + height,10) + "px"; + this.selectionHandles.styles.bottomRight.left = parseInt(left + width,10) + "px"; + + this.selectionHandles.styles.bottomRightHitArea.top = parseInt(top + height - hitAreaWidth/4,10) + "px"; + this.selectionHandles.styles.bottomRightHitArea.left = parseInt(left + width - hitAreaWidth/4,10) + "px"; + + if(this.settings.border.multipleSelectionHandlesVisible && this.settings.border.multipleSelectionHandlesVisible()) { + this.selectionHandles.styles.topLeft.display = "block"; + this.selectionHandles.styles.topLeftHitArea.display = "block"; + if(!isPartRange.call(this)) { + this.selectionHandles.styles.bottomRight.display = "block"; + this.selectionHandles.styles.bottomRightHitArea.display = "block"; + } else { + this.selectionHandles.styles.bottomRight.display = "none"; + this.selectionHandles.styles.bottomRightHitArea.display = "none"; + } + } else { + this.selectionHandles.styles.topLeft.display = "none"; + this.selectionHandles.styles.bottomRight.display = "none"; + this.selectionHandles.styles.topLeftHitArea.display = "none"; + this.selectionHandles.styles.bottomRightHitArea.display = "none"; + } + + if(fromRow == this.instance.wtSettings.getSetting('fixedRowsTop') || fromColumn == this.instance.wtSettings.getSetting('fixedColumnsLeft')) { + this.selectionHandles.styles.topLeft.zIndex = "9999"; + this.selectionHandles.styles.topLeftHitArea.zIndex = "9999"; + } else { + this.selectionHandles.styles.topLeft.zIndex = ""; + this.selectionHandles.styles.topLeftHitArea.zIndex = ""; + } + + }; + + if (instance.cloneOverlay instanceof WalkontableVerticalScrollbarNative || instance.cloneOverlay instanceof WalkontableCornerScrollbarNative) { + ilen = instance.getSetting('fixedRowsTop'); + } + else { + ilen = instance.wtTable.getRenderedRowsCount(); + } + + for (i = 0; i < ilen; i++) { + s = instance.wtTable.rowFilter.renderedToSource(i); + if (s >= corners[0] && s <= corners[2]) { + fromRow = s; + break; + } + } + + for (i = ilen - 1; i >= 0; i--) { + s = instance.wtTable.rowFilter.renderedToSource(i); + if (s >= corners[0] && s <= corners[2]) { + toRow = s; + break; + } + } + + ilen = instance.wtTable.getRenderedColumnsCount(); + + for (i = 0; i < ilen; i++) { + s = instance.wtTable.columnFilter.renderedToSource(i); + if (s >= corners[1] && s <= corners[3]) { + fromColumn = s; + break; + } + } + + for (i = ilen - 1; i >= 0; i--) { + s = instance.wtTable.columnFilter.renderedToSource(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 (Handsontable.mobileBrowser || (!this.hasSetting(this.settings.border.cornerVisible) || isPartRange.call(this))) { + this.cornerStyle.display = 'none'; + } + else { + this.cornerStyle.top = top + height - 4 + 'px'; + this.cornerStyle.left = left + width - 4 + 'px'; + this.cornerStyle.borderRightWidth = this.cornerDefaultStyle.borderWidth; + this.cornerStyle.width = this.cornerDefaultStyle.width; + this.cornerStyle.display = 'block'; + + if (!instance.cloneOverlay && toColumn === instance.wtTable.getRenderedColumnsCount() - 1) { + var scrollableElement = Handsontable.Dom.getScrollableElement(instance.wtTable.TABLE), + needShrinkCorner = toTD.offsetLeft + Handsontable.Dom.outerWidth(toTD) >= Handsontable.Dom.innerWidth(scrollableElement); + + if (needShrinkCorner) { + this.cornerStyle.borderRightWidth = '0px'; + this.cornerStyle.width = Math.ceil(parseInt(this.cornerDefaultStyle.width, 10) / 2) + 'px'; + } + } + } + + if (Handsontable.mobileBrowser) { + updateMultipleSelectionHandlesPosition.call(this, top, left, width, height); + } +}; + +/** + * 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'; + + if(Handsontable.mobileBrowser) { + this.selectionHandles.styles.topLeft.display = 'none'; + this.selectionHandles.styles.bottomRight.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; +}; + +WalkontableCellCoords.prototype.isSouthWestOf = function (testedCoords) { + return this.row >= testedCoords.row && this.col <= testedCoords.col; +}; + +WalkontableCellCoords.prototype.isNorthEastOf = 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 selected range height (in number of rows) + * @returns {number} + */ +WalkontableCellRange.prototype.getHeight = function () { + return Math.max(this.from.row, this.to.row) - Math.min(this.from.row, this.to.row) + 1; +}; + +/** + * Returns selected range width (in number of columns) + * @returns {number} + */ +WalkontableCellRange.prototype.getWidth = function () { + return Math.max(this.from.col, this.to.col) - Math.min(this.from.col, this.to.col) + 1; +}; + +/** + * 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(); + + if (cellCoords.row < 0) { + cellCoords.row = 0; + } + + if (cellCoords.col < 0) { + cellCoords.col = 0; + } + + 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()); +}; + +WalkontableCellRange.prototype.isEqual = function (testedRange) { + return (Math.min(this.from.row, this.to.row) == Math.min(testedRange.from.row, testedRange.to.row)) && + (Math.max(this.from.row, this.to.row) == Math.max(testedRange.from.row, testedRange.to.row)) && + (Math.min(this.from.col, this.to.col) == Math.min(testedRange.from.col, testedRange.to.col)) && + (Math.max(this.from.col, this.to.col) == Math.max(testedRange.from.col, testedRange.to.col)); +}; + +/** + * 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() + , bottomRight = this.getBottomRightCorner() + , topRight = this.getTopRightCorner() + , bottomLeft = this.getBottomLeftCorner(); + + 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); + + var finalFrom = new WalkontableCellCoords(resultTopRow, resultTopCol) + , finalTo = new WalkontableCellCoords(resultBottomRow, resultBottomCol); + var isCorner = new WalkontableCellRange(finalFrom, finalFrom, finalTo).isCorner(this.from, expandingRange) + , onlyMerge = expandingRange.isEqual(new WalkontableCellRange(finalFrom, finalFrom, finalTo)); + + if (isCorner && !onlyMerge) { + if (this.from.col > finalFrom.col) { + finalFrom.col = resultBottomCol; + finalTo.col = resultTopCol; + } + if (this.from.row > finalFrom.row) { + finalFrom.row = resultBottomRow; + finalTo.row = resultTopRow; + } + } + + this.from = finalFrom; + this.to = finalTo; + + return true; +}; + +WalkontableCellRange.prototype.getDirection = function () { + if (this.from.isNorthWestOf(this.to)) { // NorthWest - SouthEast + return "NW-SE"; + } else if (this.from.isNorthEastOf(this.to)) { // NorthEast - SouthWest + return "NE-SW"; + } else if (this.from.isSouthEastOf(this.to)) { // SouthEast - NorthWest + return "SE-NW"; + } else if (this.from.isSouthWestOf(this.to)) { // SouthWest - NorthEast + return "SW-NE"; + } +}; + +WalkontableCellRange.prototype.setDirection = function (direction) { + switch (direction) { + case "NW-SE" : + this.from = this.getTopLeftCorner(); + this.to = this.getBottomRightCorner(); + break; + case "NE-SW" : + this.from = this.getTopRightCorner(); + this.to = this.getBottomLeftCorner(); + break; + case "SE-NW" : + this.from = this.getBottomRightCorner(); + this.to = this.getTopLeftCorner(); + break; + case "SW-NE" : + this.from = this.getBottomLeftCorner(); + this.to = this.getTopRightCorner(); + break; + } +}; + +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.getTopRightCorner = function () { + return new WalkontableCellCoords(Math.min(this.from.row, this.to.row), Math.max(this.from.col, this.to.col)); +}; + +WalkontableCellRange.prototype.getBottomLeftCorner = function () { + return new WalkontableCellCoords(Math.max(this.from.row, this.to.row), Math.min(this.from.col, this.to.col)); +}; + +WalkontableCellRange.prototype.isCorner = function (coords, expandedRange) { + if (expandedRange) { + if (expandedRange.includes(coords)) { + if (this.getTopLeftCorner().isEqual(new WalkontableCellCoords(expandedRange.from.row, expandedRange.from.col)) || + this.getTopRightCorner().isEqual(new WalkontableCellCoords(expandedRange.from.row, expandedRange.to.col)) || + this.getBottomLeftCorner().isEqual(new WalkontableCellCoords(expandedRange.to.row, expandedRange.from.col)) || + this.getBottomRightCorner().isEqual(new WalkontableCellCoords(expandedRange.to.row, expandedRange.to.col))) { + return true; + } + } + } + return coords.isEqual(this.getTopLeftCorner()) || coords.isEqual(this.getTopRightCorner()) || coords.isEqual(this.getBottomLeftCorner()) || coords.isEqual(this.getBottomRightCorner()); +}; + +WalkontableCellRange.prototype.getOppositeCorner = function (coords, expandedRange) { + if (!(coords instanceof WalkontableCellCoords)) { + return false; + } + + if (expandedRange) { + if (expandedRange.includes(coords)) { + if (this.getTopLeftCorner().isEqual(new WalkontableCellCoords(expandedRange.from.row, expandedRange.from.col))) { + return this.getBottomRightCorner(); + } + if (this.getTopRightCorner().isEqual(new WalkontableCellCoords(expandedRange.from.row, expandedRange.to.col))) { + return this.getBottomLeftCorner(); + } + if (this.getBottomLeftCorner().isEqual(new WalkontableCellCoords(expandedRange.to.row, expandedRange.from.col))) { + return this.getTopRightCorner(); + } + if (this.getBottomRightCorner().isEqual(new WalkontableCellCoords(expandedRange.to.row, expandedRange.to.col))) { + return this.getTopLeftCorner(); + } + } + } + + if (coords.isEqual(this.getBottomRightCorner())) { + return this.getTopLeftCorner(); + } else if (coords.isEqual(this.getTopLeftCorner())) { + return this.getBottomRightCorner(); + } else if (coords.isEqual(this.getTopRightCorner())) { + return this.getBottomLeftCorner(); + } else if (coords.isEqual(this.getBottomLeftCorner())) { + return this.getTopRightCorner(); + } +}; + +WalkontableCellRange.prototype.getBordersSharedWith = function (range) { + if (!this.includesRange(range)) { + return []; + } + + var thisBorders = { + top: Math.min(this.from.row, this.to.row), + bottom: Math.max(this.from.row, this.to.row), + left: Math.min(this.from.col, this.to.col), + right: Math.max(this.from.col, this.to.col) + } + , rangeBorders = { + top: Math.min(range.from.row, range.to.row), + bottom: Math.max(range.from.row, range.to.row), + left: Math.min(range.from.col, range.to.col), + right: Math.max(range.from.col, range.to.col) + } + , result = []; + + if (thisBorders.top == rangeBorders.top) { + result.push('top'); + } + if (thisBorders.right == rangeBorders.right) { + result.push('right'); + } + if (thisBorders.bottom == rangeBorders.bottom) { + result.push('bottom'); + } + if (thisBorders.left == rangeBorders.left) { + result.push('left'); + } + + return result; +}; + +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 + +/** + * WalkontableColumnFilter + * @constructor + */ +function WalkontableColumnFilter(offset,total, countTH) { + this.offset = offset; + this.total = total; + this.countTH = countTH; +} + +WalkontableColumnFilter.prototype.offsetted = function (n) { + return n + this.offset; +}; + +WalkontableColumnFilter.prototype.unOffsetted = function (n) { + return n - this.offset; +}; + +WalkontableColumnFilter.prototype.renderedToSource = function (n) { + return this.offsetted(n); +}; + +WalkontableColumnFilter.prototype.sourceToRendered = function (n) { + return this.unOffsetted(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.renderedToSource(this.offsettedTH(n)); +}; + +WalkontableColumnFilter.prototype.sourceColumnToVisibleRowHeadedColumn = function (n) { + return this.unOffsettedTH(this.sourceToRendered(n)); +}; + +/** + * WalkontableColumnStrategy + * @param containerSizeFn + * @param sizeAtIndex + * @param strategy - all, last, none + * @constructor + */ +function WalkontableColumnStrategy(instance, containerSizeFn, sizeAtIndex, strategy) { + var size + , i = 0; + + this.instance = instance; + 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.visibleCellCount++; + } + this.cellSizes.push(size); + this.cellSizesSum += size; + this.cellCount++; + + i++; + } + + var containerSize = this.getContainerSize(); + 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.getContainerSize = function () { + return typeof this.containerSizeFn === 'function' ? this.containerSizeFn() : this.containerSizeFn; +}; + +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() + , 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 && 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.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); + this.selections = this.getSetting('selections'); + + 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; +} + +/** + * Force rerender of Walkontable + * @param fastDraw {Boolean} When TRUE, try to refresh only the positions of borders without rerendering the data. + * It will only work if WalkontableTable.draw() does not force rendering anyway + * @returns {Walkontable} + */ +Walkontable.prototype.draw = function (fastDraw) { + this.drawInterrupted = false; + if (!fastDraw && !Handsontable.Dom.isVisible(this.wtTable.TABLE)) { + this.drawInterrupted = true; //draw interrupted because TABLE is not visible + return; + } + + this.wtTable.draw(fastDraw); + + return this; +}; + +/** + * Returns the TD at coords. If topmost is set to true, returns TD from the topmost overlay layer, + * if not set or set to false, returns TD from the master table. + * @param {WalkontableCellCoords} coords + * @param {Boolean} topmost + * @returns {Object} + */ +Walkontable.prototype.getCell = function (coords, topmost) { + if(!topmost) { + return this.wtTable.getCell(coords); + } else { + var fixedRows = this.wtSettings.getSetting('fixedRowsTop') + , fixedColumns = this.wtSettings.getSetting('fixedColumnsLeft'); + + if(coords.row < fixedRows && coords.col < fixedColumns) { + return this.wtScrollbars.corner.clone.wtTable.getCell(coords); + } else if(coords.row < fixedRows) { + return this.wtScrollbars.vertical.clone.wtTable.getCell(coords); + } else if (coords.col < fixedColumns) { + return this.wtScrollbars.horizontal.clone.wtTable.getCell(coords); + } else { + return this.wtTable.getCell(coords); + } + } +}; + +Walkontable.prototype.update = function (settings, value) { + return this.wtSettings.update(settings, value); +}; + +/** + * Scroll the viewport to a row at the given index in the data source + * @param row + * @returns {Walkontable} + */ +Walkontable.prototype.scrollVertical = function (row) { + this.wtScrollbars.vertical.scrollTo(row); + this.getSetting('onScrollVertically'); + return this; +}; + +/** + * Scroll the viewport to a column at the given index in the data source + * @param row + * @returns {Walkontable} + */ +Walkontable.prototype.scrollHorizontal = function (column) { + this.wtScrollbars.horizontal.scrollTo(column); + this.getSetting('onScrollHorizontally'); + return this; +}; + +/** + * 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/handsontable/wiki/JavaScript-&-DOM-performance-tips +}; + +Walkontable.prototype.hasSetting = function (key) { + return this.wtSettings.has(key); +}; + +Walkontable.prototype.destroy = function () { + this.wtScrollbars.destroy(); + + if ( 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; + + Handsontable.Dom.addClass(this.clone.wtTable.holder.parentNode, 'wtDebugVisible'); + + /*var that = this; + var lastX = 0; + var lastY = 0; + var overlayContainer = that.clone.wtTable.holder.parentNode; + + var eventManager = Handsontable.eventManager(instance); + + eventManager.addEventListener(document.body, 'mousemove', 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.destroy = function () { + WalkontableOverlay.prototype.destroy.call(this); + clearTimeout(this.lastTimeout); +}; + +function WalkontableEvent(instance) { + var that = this; + + var eventManager = Handsontable.eventManager(instance); + + //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 onTouchMove = function (event) { + that.instance.touchMoving = true; + }; + + var longTouchTimeout; + + ///** + // * Update touch event target - if user taps on resize handle 'hit area', update the target to the cell itself + // * @param event + // */ + /* + var adjustTapTarget = function (event) { + var currentSelection + , properTarget; + + if(Handsontable.Dom.hasClass(event.target,'SelectionHandle')) { + if(that.instance.selections[0].cellRange) { + currentSelection = that.instance.selections[0].cellRange.highlight; + + properTarget = that.instance.getCell(currentSelection, true); + } + } + + if(properTarget) { + Object.defineProperty(event,'target',{ + value: properTarget + }); + } + + return event; + };*/ + + var onTouchStart = function (event) { + var container = this; + + eventManager.addEventListener(this, 'touchmove', onTouchMove); + + //this.addEventListener("touchmove", onTouchMove, false); + + // touch-and-hold event + //longTouchTimeout = setTimeout(function () { + // if(!that.instance.touchMoving) { + // that.instance.longTouch = true; + // + // var targetCoords = Handsontable.Dom.offset(event.target); + // var contextMenuEvent = new MouseEvent('contextmenu', { + // clientX: targetCoords.left + event.target.offsetWidth, + // clientY: targetCoords.top + event.target.offsetHeight, + // button: 2 + // }); + // + // that.instance.wtTable.holder.parentNode.parentNode.dispatchEvent(contextMenuEvent); + // } + //},200); + + // Prevent cell selection when scrolling with touch event - not the best solution performance-wise + that.checkIfTouchMove = setTimeout(function () { + if (that.instance.touchMoving === true) { + that.instance.touchMoving = void 0; + + eventManager.removeEventListener("touchmove", onTouchMove, false); + + return; + } else { + //event = adjustTapTarget(event); + + onMouseDown(event); + } + }, 30); + + //eventManager.removeEventListener(that.instance.wtTable.holder, "mousedown", onMouseDown); + }; + + 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); + } + } + }; + + + var onTouchEnd = function (event) { + clearTimeout(longTouchTimeout); + //that.instance.longTouch == void 0; + + event.preventDefault(); + + onMouseUp(event); + + //eventManager.removeEventListener(that.instance.wtTable.holder, "mouseup", onMouseUp); + }; + + eventManager.addEventListener(this.instance.wtTable.holder, 'mousedown', onMouseDown); + + eventManager.addEventListener(this.instance.wtTable.TABLE, 'mouseover', onMouseOver); + + eventManager.addEventListener(this.instance.wtTable.holder, 'mouseup', onMouseUp); + + + if(this.instance.wtTable.holder.parentNode.parentNode && Handsontable.mobileBrowser) { // check if full HOT instance, or detached WOT AND run on mobile device + var classSelector = "." + this.instance.wtTable.holder.parentNode.className.split(" ").join("."); + + eventManager.addEventListener(this.instance.wtTable.holder.parentNode.parentNode, 'touchstart', function (event) { + that.instance.touchApplied = true; + if (Handsontable.Dom.isChildOf(event.target, classSelector)) { + onTouchStart.call(event.target, event); + } + }); + eventManager.addEventListener(this.instance.wtTable.holder.parentNode.parentNode, 'touchend', function (event) { + that.instance.touchApplied = false; + if (Handsontable.Dom.isChildOf(event.target, classSelector)) { + onTouchEnd.call(event.target, event); + } + }); + + if(!that.instance.momentumScrolling) { + that.instance.momentumScrolling = {}; + } + eventManager.addEventListener(this.instance.wtTable.holder.parentNode.parentNode, 'scroll', function (event) { + clearTimeout(that.instance.momentumScrolling._timeout); + + if(!that.instance.momentumScrolling.ongoing) { + that.instance.getSetting('onBeforeTouchScroll'); + } + that.instance.momentumScrolling.ongoing = true; + + that.instance.momentumScrolling._timeout = setTimeout(function () { + if(!that.instance.touchApplied) { + that.instance.momentumScrolling.ongoing = false; + + that.instance.getSetting('onAfterMomentumScroll'); + } + },200); + }); + } + + eventManager.addEventListener(window, 'resize', function () { + that.instance.draw(); + }); + + this.destroy = function () { + clearTimeout(this.dblClickTimeout[0]); + clearTimeout(this.dblClickTimeout[1]); + + eventManager.clear(); + }; +} + +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; //selections.current is current selected cell + cell.TD = this.instance.wtTable.getCell(cell.coords); + } else if (Handsontable.Dom.hasClass(elem, 'wtBorder') && Handsontable.Dom.hasClass(elem, 'area')) { + if (this.instance.selections.area.cellRange) { + cell.coords = this.instance.selections.area.cellRange.to; //selections.area is area selected cells + 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; +} + +/** + * 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; + /* jshint -W121 */ + 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.renderedToSource = function (n) { + return this.offsetted(n); +}; + +WalkontableRowFilter.prototype.sourceToRendered = function (n) { + return this.unOffsetted(n); +}; + +WalkontableRowFilter.prototype.offsettedTH = function (n) { + return n - this.countTH; +}; + +WalkontableRowFilter.prototype.visibleColHeadedRowToSourceRow = function (n) { + return this.renderedToSource(this.offsettedTH(n)); +}; + +WalkontableRowFilter.prototype.sourceRowToVisibleColHeadedRow = function (n) { + return this.unOffsettedTH(this.sourceToRendered(n)); +}; + +function WalkontableScroll(instance) { + this.instance = instance; +} + +/** + * Scrolls viewport to a cell by minimum number of cells + * @param {WalkontableCellCoords} coords + */ +WalkontableScroll.prototype.scrollViewport = function (coords) { + if (!this.instance.drawn) { + return; + } + + var 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'); + } + + if (coords.row > this.instance.wtTable.getLastVisibleRow()) { + this.instance.wtScrollbars.vertical.scrollTo(coords.row, true); + } else if (coords.row >= this.instance.getSetting('fixedRowsTop') && coords.row < this.instance.wtTable.getFirstVisibleRow()){ + this.instance.wtScrollbars.vertical.scrollTo(coords.row); + } + + if (coords.col > this.instance.wtTable.getLastVisibleColumn()) { + this.instance.wtScrollbars.horizontal.scrollTo(coords.col, true); + } else if (coords.col > this.instance.getSetting('fixedColumnsLeft') && coords.col < this.instance.wtTable.getFirstVisibleColumn()){ + this.instance.wtScrollbars.horizontal.scrollTo(coords.col); + } + + //} +}; + +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, + finalLeft, + finalTop; + + if (this.scrollHandler === window) { + var box = this.instance.wtTable.holder.getBoundingClientRect(); + var top = Math.ceil(box.top); + var left = Math.ceil(box.left); + var bottom = Math.ceil(box.bottom); + var right = Math.ceil(box.right); + + if (left < 0 && (right - elem.offsetWidth) > 0) { + finalLeft = -left + 'px'; + } else { + finalLeft = '0'; + } + + if (top < 0 && (bottom - elem.offsetHeight) > 0) { + finalTop = -top + "px"; + } else { + finalTop = "0"; + } + } + else if(!Handsontable.freezeOverlays) { + finalLeft = this.instance.wtScrollbars.horizontal.getScrollPosition() + "px"; + finalTop = this.instance.wtScrollbars.vertical.getScrollPosition() + "px"; + } + + Handsontable.Dom.setOverlayPosition(elem, finalLeft, finalTop); + + elem.style.width = Handsontable.Dom.outerWidth(this.clone.wtTable.TABLE) + 4 + 'px'; + elem.style.height = Handsontable.Dom.outerHeight(this.clone.wtTable.TABLE) + 4 + 'px'; +}; + +function WalkontableHorizontalScrollbarNative(instance) { + this.instance = instance; + this.type = 'horizontal'; + 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 () { + var finalLeft, finalTop; + + 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); + var right = Math.ceil(box.right); + + if (left < 0 && (right - elem.offsetWidth) > 0) { + finalLeft = -left + 'px'; + } else { + finalLeft = '0'; + } + + finalTop = this.instance.wtTable.hider.style.top; + } + else if(!Handsontable.freezeOverlays) { + finalLeft = this.getScrollPosition() + "px"; + finalTop = this.instance.wtTable.hider.style.top; + } + + Handsontable.Dom.setOverlayPosition(elem, finalLeft, finalTop); + + elem.style.height = Handsontable.Dom.outerHeight(this.clone.wtTable.TABLE) + 'px'; + elem.style.width = Handsontable.Dom.outerWidth(this.clone.wtTable.TABLE) + 4 + 'px';// + 4 + 'px'; +}; + +WalkontableHorizontalScrollbarNative.prototype.refresh = function (fastDraw) { + this.applyToDOM(); + WalkontableOverlay.prototype.refresh.call(this, fastDraw); +}; + +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 () { + this.instance.getSetting('onScrollHorizontally'); +}; + +WalkontableHorizontalScrollbarNative.prototype.sumCellSizes = function (from, length) { + var sum = 0; + while(from < length) { + sum += this.instance.wtTable.getStretchedColumnWidth(from) || this.instance.wtSettings.defaultColumnWidth; + from++; + } + return sum; +}; + +//applyToDOM (in future merge it with this.refresh?) +WalkontableHorizontalScrollbarNative.prototype.applyToDOM = function () { + var total = this.instance.getSetting('totalColumns'); + var headerSize = this.instance.wtViewport.getRowHeaderWidth(); + + this.fixedContainer.style.width = headerSize + this.sumCellSizes(0, total) + 'px';// + 4 + 'px'; + + if (typeof this.instance.wtViewport.columnsRenderCalculator.startPosition === 'number'){ + this.fixed.style.left = this.instance.wtViewport.columnsRenderCalculator.startPosition + 'px'; + } + else if (total === 0) { + this.fixed.style.left = '0'; + } else { + throw new Error('Incorrect value of the columnsRenderCalculator'); + } + this.fixed.style.right = ''; +}; + +/** + * Scrolls horizontally to a column at the left edge of the viewport + * @param sourceCol {Number} + * @param beyondRendered {Boolean} if TRUE, scrolls according to the bottom edge (top edge is by default) + */ +WalkontableHorizontalScrollbarNative.prototype.scrollTo = function (sourceCol, beyondRendered) { + var newX = this.getTableParentOffset(); + + if (beyondRendered) { + newX += this.sumCellSizes(0, sourceCol + 1); + newX -= this.instance.wtViewport.getViewportWidth(); + } + else { + var fixedColumnsLeft = this.instance.getSetting('fixedColumnsLeft'); + newX += this.sumCellSizes(fixedColumnsLeft, sourceCol); + } + + this.setScrollPosition(newX); +}; + +WalkontableHorizontalScrollbarNative.prototype.getTableParentOffset = function () { + if (this.scrollHandler === window) { + return this.instance.wtTable.holderOffset.left; + } + else { + return 0; + } +}; + +function WalkontableVerticalScrollbarNative(instance) { + this.instance = instance; + this.type = 'vertical'; + this.init(); + this.clone = this.makeClone('top'); +} + +WalkontableVerticalScrollbarNative.prototype = new WalkontableOverlay(); + +//resetFixedPosition (in future merge it with this.refresh?) +WalkontableVerticalScrollbarNative.prototype.resetFixedPosition = function () { + var finalLeft, finalTop; + + 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 bottom = Math.ceil(box.bottom); + + finalLeft = this.instance.wtTable.hider.style.left; + + if (top < 0 && (bottom - elem.offsetHeight) > 0) { + finalTop = -top + "px"; + } else { + finalTop = "0"; + } + } + else if(!Handsontable.freezeOverlays) { + finalTop = this.getScrollPosition() + "px"; + finalLeft = this.instance.wtTable.hider.style.left; + } + + Handsontable.Dom.setOverlayPosition(elem, finalLeft, finalTop); + + 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';// + 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 () { + this.instance.getSetting('onScrollVertically'); +}; + +WalkontableVerticalScrollbarNative.prototype.sumCellSizes = function (from, length) { + var sum = 0; + while (from < length) { + sum += this.instance.wtTable.getRowHeight(from) || this.instance.wtSettings.settings.defaultRowHeight; //TODO optimize getSetting, because this is MUCH faster then getSetting + from++; + } + return sum; +}; + +WalkontableVerticalScrollbarNative.prototype.refresh = function (fastDraw) { + this.applyToDOM(); + WalkontableOverlay.prototype.refresh.call(this, fastDraw); +}; + +//applyToDOM (in future merge it with this.refresh?) +WalkontableVerticalScrollbarNative.prototype.applyToDOM = function () { + var total = this.instance.getSetting('totalRows'); + var headerSize = this.instance.wtViewport.getColumnHeaderHeight(); + + this.fixedContainer.style.height = headerSize + this.sumCellSizes(0, total) + 'px'; + if (typeof this.instance.wtViewport.rowsRenderCalculator.startPosition === 'number') { + this.fixed.style.top = this.instance.wtViewport.rowsRenderCalculator.startPosition + 'px'; + } + else if (total === 0) { + this.fixed.style.top = '0'; //can happen if there are 0 rows + } + else { + throw new Error("Incorrect value of the rowsRenderCalculator"); + } + this.fixed.style.bottom = ''; +}; + +/** + * Scrolls vertically to a row + * + * @param sourceRow {Number} + * @param bottomEdge {Boolean} if TRUE, scrolls according to the bottom edge (top edge is by default) + */ +WalkontableVerticalScrollbarNative.prototype.scrollTo = function (sourceRow, bottomEdge) { + var newY = this.getTableParentOffset(); + + if (bottomEdge) { + newY += this.sumCellSizes(0, sourceRow + 1); + newY -= this.instance.wtViewport.getViewportHeight(); + // Fix 1 pixel offset when cell is selected + newY += 1; + } + else { + var fixedRowsTop = this.instance.getSetting('fixedRowsTop'); + newY += this.sumCellSizes(fixedRowsTop, sourceRow); + } + + this.setScrollPosition(newY); +}; + +WalkontableVerticalScrollbarNative.prototype.getTableParentOffset = function () { + if (this.scrollHandler === window) { + return this.instance.wtTable.holderOffset.top; + } + else { + return 0; + } +}; + +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; + + this.refreshAll = 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.instance.draw(true); + that.vertical.onScroll(); + that.horizontal.onScroll(); + }; + + var eventManager = Handsontable.eventManager(that.instance); + + eventManager.addEventListener(this.vertical.scrollHandler, 'scroll', this.refreshAll); + if (this.vertical.scrollHandler !== this.horizontal.scrollHandler) { + eventManager.addEventListener(this.horizontal.scrollHandler, 'scroll', this.refreshAll); + } + + if (this.vertical.scrollHandler !== window && this.horizontal.scrollHandler !== window) { + eventManager.addEventListener(window,'scroll', this.refreshAll); + } +}; + +WalkontableScrollbars.prototype.destroy = function () { + var eventManager = Handsontable.eventManager(this.instance); + + if (this.vertical) { + this.vertical.destroy(); + eventManager.removeEventListener(this.vertical.scrollHandler,'scroll', this.refreshAll); + } + if (this.horizontal) { + this.horizontal.destroy(); + eventManager.removeEventListener(this.horizontal.scrollHandler,'scroll', this.refreshAll); + } + eventManager.removeEventListener(window,'scroll', this.refreshAll); + if (this.corner ) { + this.corner.destroy(); + } + if (this.debug) { + this.debug.destroy(); + } +}; + +WalkontableScrollbars.prototype.refresh = function (fastDraw) { + if (this.horizontal) { + this.horizontal.refresh(fastDraw); + } + if (this.vertical) { + this.vertical.refresh(fastDraw); + } + if (this.corner) { + this.corner.refresh(fastDraw); + } + if (this.debug) { + this.debug.refresh(fastDraw); + } +}; + +WalkontableScrollbars.prototype.applyToDOM = function () { + if (this.horizontal) { + this.horizontal.applyToDOM(); + } + if (this.vertical) { + this.vertical.applyToDOM(); + } +}; + +function WalkontableSelection(settings, cellRange) { + this.settings = settings; + this.cellRange = cellRange || null; + this.instanceBorders = {}; +} + +/** + * Each Walkontable clone requires it's own border for every selection. This method creates and returns selection borders per instance + * @param {Walkontable} instance + * @returns {WalkontableBorder} + */ +WalkontableSelection.prototype.getBorder = function (instance) { + if (this.instanceBorders[instance.guid]) { + return this.instanceBorders[instance.guid]; + } + //where is this returned? + this.instanceBorders[instance.guid] = new WalkontableBorder(instance, this.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(), + bottomRight = this.cellRange.getBottomRightCorner(); + + return [topLeft.row, topLeft.col, bottomRight.row, bottomRight.col]; +}; + +WalkontableSelection.prototype.addClassAtCoords = function (instance, sourceRow, sourceColumn, cls) { + var TD = instance.wtTable.getCell(new WalkontableCellCoords(sourceRow, sourceColumn)); + + if (typeof TD === 'object') { + Handsontable.Dom.addClass(TD, cls); + } +}; + +WalkontableSelection.prototype.draw = function (instance) { + var + _this = this, + renderedRows = instance.wtTable.getRenderedRowsCount(), + renderedColumns = instance.wtTable.getRenderedColumnsCount(), + corners, sourceRow, sourceCol, border, TH; + + if (this.isEmpty()) { + if (this.settings.border) { + border = this.getBorder(instance); + + if (border) { + border.disappear(); + } + } + + return; + } + + corners = this.getCorners(); + + for (var column = 0; column < renderedColumns; column++) { + sourceCol = instance.wtTable.columnFilter.renderedToSource(column); + + if (sourceCol >= corners[1] && sourceCol <= corners[3]) { + TH = instance.wtTable.getColumnHeader(sourceCol); + + if (TH && _this.settings.highlightColumnClassName) { + Handsontable.Dom.addClass(TH, _this.settings.highlightColumnClassName); + } + } + } + + for (var row = 0; row < renderedRows; row++) { + sourceRow = instance.wtTable.rowFilter.renderedToSource(row); + + if (sourceRow >= corners[0] && sourceRow <= corners[2]) { + TH = instance.wtTable.getRowHeader(sourceRow); + + if (TH && _this.settings.highlightRowClassName) { + Handsontable.Dom.addClass(TH, _this.settings.highlightRowClassName); + } + } + + for (var column = 0; column < renderedColumns; column++) { + sourceCol = instance.wtTable.columnFilter.renderedToSource(column); + + if (sourceRow >= corners[0] && sourceRow <= corners[2] && sourceCol >= corners[1] && sourceCol <= corners[3]) { + // selected cell + if (_this.settings.className) { + _this.addClassAtCoords(instance, sourceRow, sourceCol, _this.settings.className); + } + } + else if (sourceRow >= corners[0] && sourceRow <= corners[2]) { + // selection is in this row + if (_this.settings.highlightRowClassName) { + _this.addClassAtCoords(instance, sourceRow, sourceCol, _this.settings.highlightRowClassName); + } + } + else if (sourceCol >= corners[1] && sourceCol <= corners[3]) { + // selection is in this column + if (_this.settings.highlightColumnClassName) { + _this.addClassAtCoords(instance, sourceRow, sourceCol, _this.settings.highlightColumnClassName); + } + } + } + } + + instance.getSetting('onBeforeDrawBorders', corners, this.settings.className); + + if (this.settings.border) { + border = this.getBorder(instance); + + if (border) { + // warning! border.appear modifies corners! + border.appear(corners); + } + } +}; + +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, + 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, + cellRenderer: function (row, column, TD) { + var cellData = that.getSetting('data', row, column); + Handsontable.Dom.fastInnerText(TD, cellData === void 0 || cellData === null ? '' : cellData); + }, + //columnWidth: 50, + columnWidth: function (col) { + return; //return undefined means use default size for the rendered cell content + }, + rowHeight: function (row) { + return; //return undefined means use default size for the rendered cell content + }, + defaultRowHeight: 23, + defaultColumnWidth: 50, + selections: null, + hideBorderOnMouseDownOver: false, + viewportRowCalculatorOverride: null, + viewportColumnCalculatorOverride: null, + + //callbacks + onCellMouseDown: null, + onCellMouseOver: null, +// onCellMouseOut: null, + onCellDblClick: null, + onCellCornerMouseDown: null, + onCellCornerDblClick: null, + beforeDraw: null, + onDraw: null, + onBeforeDrawBorders: null, + onScrollVertically: null, + onScrollHorizontally: null, + onBeforeTouchScroll: null, + onAfterMomentumScroll: null, + + //constants + scrollbarWidth: 10, + scrollbarHeight: 10, + + renderAllRows: false, + groups: 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/handsontable/wiki/JavaScript-&-DOM-performance-tips + } + else if (param1 !== void 0 && Array.isArray(this.settings[key])) { //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; + + if (!this.isWorkingOnClone()) { + this.holder.parentNode.style.position = "relative"; + } + + //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.rowFilter = null; + this.columnFilter = null; +} + +WalkontableTable.prototype.isWorkingOnClone = function () { + return !!this.instance.cloneSource; +}; + +/** + * Redraws the table + * @param fastDraw {Boolean} If TRUE, will try to avoid full redraw and only update the border positions. If FALSE or UNDEFINED, will perform a full redraw + * @returns {WalkontableTable} + */ +WalkontableTable.prototype.draw = function (fastDraw) { + if (!this.isWorkingOnClone()) { + this.holderOffset = Handsontable.Dom.offset(this.holder); + fastDraw = this.instance.wtViewport.createRenderCalculators(fastDraw); + } + + if (!fastDraw) { + if (this.isWorkingOnClone()) { + this.tableOffset = this.instance.cloneSource.wtTable.tableOffset; + } + else { + this.tableOffset = Handsontable.Dom.offset(this.TABLE); + } + var startRow; + if (this.instance.cloneOverlay instanceof WalkontableDebugOverlay || + this.instance.cloneOverlay instanceof WalkontableVerticalScrollbarNative || + this.instance.cloneOverlay instanceof WalkontableCornerScrollbarNative) { + startRow = 0; + } + else { + startRow = this.instance.wtViewport.rowsRenderCalculator.startRow; + } + + + var startColumn; + if (this.instance.cloneOverlay instanceof WalkontableDebugOverlay || + this.instance.cloneOverlay instanceof WalkontableHorizontalScrollbarNative || + this.instance.cloneOverlay instanceof WalkontableCornerScrollbarNative) { + startColumn = 0; + } else { + startColumn = this.instance.wtViewport.columnsRenderCalculator.startColumn; + } + + this.rowFilter = new WalkontableRowFilter( + startRow, + this.instance.getSetting('totalRows'), + this.instance.getSetting('columnHeaders').length + ); + this.columnFilter = new WalkontableColumnFilter( + startColumn, + this.instance.getSetting('totalColumns'), + this.instance.getSetting('rowHeaders').length + ); + this._doDraw(); //creates calculator after draw + } + else { + if (!this.isWorkingOnClone()) { + //in case we only scrolled without redraw, update visible rows information in oldRowsCalculator + this.instance.wtViewport.createVisibleCalculators(); + } + if (this.instance.wtScrollbars) { + this.instance.wtScrollbars.refresh(true); + } + } + + this.refreshSelections(fastDraw); + + if (!this.isWorkingOnClone()) { + this.instance.wtScrollbars.vertical.resetFixedPosition(); + this.instance.wtScrollbars.horizontal.resetFixedPosition(); + this.instance.wtScrollbars.corner.resetFixedPosition(); + } + + this.instance.drawn = true; + return this; +}; + +WalkontableTable.prototype._doDraw = function () { + var wtRenderer = new WalkontableTableRenderer(this); + wtRenderer.render(); +}; + +WalkontableTable.prototype.removeClassFromCells = function (className) { + var nodes = this.TABLE.querySelectorAll('.' + className); + for (var i = 0, ilen = nodes.length; i < ilen; i++) { + Handsontable.Dom.removeClass(nodes[i], className); + } +}; + +WalkontableTable.prototype.refreshSelections = function (fastDraw) { + var i, len; + + if (!this.instance.selections) { + return; + } + len = this.instance.selections.length; + + if (fastDraw) { + for (i = 0; i < len; i++) { + // there was no rerender, so we need to remove classNames by ourselves + if (this.instance.selections[i].settings.className) { + this.removeClassFromCells(this.instance.selections[i].settings.className); + } + if (this.instance.selections[i].settings.highlightRowClassName) { + this.removeClassFromCells(this.instance.selections[i].settings.highlightRowClassName); + } + if (this.instance.selections[i].settings.highlightColumnClassName) { + this.removeClassFromCells(this.instance.selections[i].settings.highlightColumnClassName); + } + } + } + for (i = 0; i < len; i++) { + this.instance.selections[i].draw(this.instance, fastDraw); + } +}; + +/** + * 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.isRowBeforeRenderedRows(coords.row)) { + return -1; //row before rendered rows + } + else if (this.isRowAfterRenderedRows(coords.row)) { + return -2; //row after rendered rows + } + + var TR = this.TBODY.childNodes[this.rowFilter.sourceToRendered(coords.row)]; + + if (TR) { + return TR.childNodes[this.columnFilter.sourceColumnToVisibleRowHeadedColumn(coords.col)]; + } +}; + +/** + * getColumnHeader + * @param col + * @param level Header level (0 = most distant to the table) + * @return {Object} HTMLElement on success or undefined on error + * + */ +WalkontableTable.prototype.getColumnHeader = function(col, level) { + if(!level) { + level = 0; + } + + var TR = this.THEAD.childNodes[level]; + if (TR) { + return TR.childNodes[this.columnFilter.sourceColumnToVisibleRowHeadedColumn(col)]; + } +}; + +/** + * getRowHeader + * @param row + * @return {Object} HTMLElement on success or {Number} one of the exit codes on error: + * null table doesn't have row headers + * + */ +WalkontableTable.prototype.getRowHeader = function(row) { + if(this.columnFilter.sourceColumnToVisibleRowHeadedColumn(0) === 0) { + return null; + } + + var TR = this.TBODY.childNodes[this.rowFilter.sourceToRendered(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.renderedToSource(row); + } + + return new WalkontableCellCoords( + row, + this.columnFilter.visibleRowHeadedColumnToSourceColumn(TD.cellIndex) + ); +}; + +WalkontableTable.prototype.getTrForRow = function (row) { + return this.TBODY.childNodes[this.rowFilter.sourceToRendered(row)]; +}; + +WalkontableTable.prototype.getFirstRenderedRow = function () { + return this.instance.wtViewport.rowsRenderCalculator.startRow; +}; + +WalkontableTable.prototype.getFirstVisibleRow = function () { + return this.instance.wtViewport.rowsVisibleCalculator.startRow; +}; + +WalkontableTable.prototype.getFirstRenderedColumn = function () { + return this.instance.wtViewport.columnsRenderCalculator.startColumn; +}; + +//returns -1 if no column is visible +WalkontableTable.prototype.getFirstVisibleColumn = function () { + return this.instance.wtViewport.columnsVisibleCalculator.startColumn; +}; + +//returns -1 if no row is visible +WalkontableTable.prototype.getLastRenderedRow = function () { + return this.instance.wtViewport.rowsRenderCalculator.endRow; +}; + +WalkontableTable.prototype.getLastVisibleRow = function () { + return this.instance.wtViewport.rowsVisibleCalculator.endRow; +}; + +WalkontableTable.prototype.getLastRenderedColumn = function () { + return this.instance.wtViewport.columnsRenderCalculator.endColumn; +}; + +//returns -1 if no column is visible +WalkontableTable.prototype.getLastVisibleColumn = function () { + return this.instance.wtViewport.columnsVisibleCalculator.endColumn; +}; + +WalkontableTable.prototype.isRowBeforeRenderedRows = function (r) { + return (this.rowFilter.sourceToRendered(r) < 0 && r >= 0); +}; + +WalkontableTable.prototype.isRowAfterViewport = function (r) { + return (r > this.getLastVisibleRow()); +}; + +WalkontableTable.prototype.isRowAfterRenderedRows = function (r) { + return (r > this.getLastRenderedRow()); +}; + +WalkontableTable.prototype.isColumnBeforeViewport = function (c) { + return (this.columnFilter.sourceToRendered(c) < 0 && c >= 0); +}; + +WalkontableTable.prototype.isColumnAfterViewport = function (c) { + return (c > this.getLastVisibleColumn()); +}; + +WalkontableTable.prototype.isLastRowFullyVisible = function () { + return (this.getLastVisibleRow() === this.getLastRenderedRow()); +}; + +WalkontableTable.prototype.isLastColumnFullyVisible = function () { + return (this.getLastVisibleColumn() === this.getLastRenderedColumn); +}; + +WalkontableTable.prototype.getRenderedColumnsCount = function () { + if (this.instance.cloneOverlay instanceof WalkontableDebugOverlay) { + return this.instance.getSetting('totalColumns'); + } + else if (this.instance.cloneOverlay instanceof WalkontableHorizontalScrollbarNative || this.instance.cloneOverlay instanceof WalkontableCornerScrollbarNative) { + return this.instance.getSetting('fixedColumnsLeft'); + } + else { + return this.instance.wtViewport.columnsRenderCalculator.count; + } +}; + +WalkontableTable.prototype.getRenderedRowsCount = function () { + if (this.instance.cloneOverlay instanceof WalkontableDebugOverlay) { + return this.instance.getSetting('totalRows'); + } + else if (this.instance.cloneOverlay instanceof WalkontableVerticalScrollbarNative || this.instance.cloneOverlay instanceof WalkontableCornerScrollbarNative) { + return this.instance.getSetting('fixedRowsTop'); + } + return this.instance.wtViewport.rowsRenderCalculator.count; +}; + +WalkontableTable.prototype.getVisibleRowsCount = function () { + return this.instance.wtViewport.rowsVisibleCalculator.count; +}; + +WalkontableTable.prototype.allRowsInViewport = function () { + return this.instance.getSetting('totalRows') == this.getVisibleRowsCount(); +}; + +/** + * Checks if any of the row's cells content exceeds its initial height, and if so, returns the oversized height + * @param {Number} sourceRow + * @return {Number} + */ +WalkontableTable.prototype.getRowHeight = function (sourceRow) { + var height = this.instance.wtSettings.settings.rowHeight(sourceRow); + var oversizedHeight = this.instance.wtViewport.oversizedRows[sourceRow]; + if (oversizedHeight !== void 0) { + height = height ? Math.max(height, oversizedHeight) : oversizedHeight; + } + return height; +}; + +WalkontableTable.prototype.getColumnHeaderHeight = function (level) { + var height = this.instance.wtSettings.settings.defaultRowHeight, + oversizedHeight = this.instance.wtViewport.oversizedColumnHeaders[level]; + if (oversizedHeight !== void 0) { + height = height ? Math.max(height, oversizedHeight) : oversizedHeight; + } + return height; +}; + +WalkontableTable.prototype.getVisibleColumnsCount = function () { + return this.instance.wtViewport.columnsVisibleCalculator.count; +}; + + +WalkontableTable.prototype.allColumnsInViewport = function () { + return this.instance.getSetting('totalColumns') == this.getVisibleColumnsCount(); +}; + + + +WalkontableTable.prototype.getColumnWidth = function (sourceColumn) { + var width = this.instance.wtSettings.settings.columnWidth; + if(typeof width === 'function') { + width = width(sourceColumn); + } else if(typeof width === 'object') { + width = width[sourceColumn]; + } + + var oversizedWidth = this.instance.wtViewport.oversizedCols[sourceColumn]; + if (oversizedWidth !== void 0) { + width = width ? Math.max(width, oversizedWidth) : oversizedWidth; + } + return width; +}; + +WalkontableTable.prototype.getStretchedColumnWidth = function (sourceColumn) { + var + width = this.getColumnWidth(sourceColumn) || this.instance.wtSettings.settings.defaultColumnWidth, + calculator = this.instance.wtViewport.columnsRenderCalculator, + stretchedWidth; + + if (calculator) { + stretchedWidth = calculator.getStretchedColumnWidth(sourceColumn, width); + + if (stretchedWidth) { + width = stretchedWidth; + } + } + + return width; +}; + + +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'); + this.columnHeaderCount = this.columnHeaders.length; + + var visibleColIndex + , totalRows = this.instance.getSetting('totalRows') + , totalColumns = this.instance.getSetting('totalColumns') + , displayTds + , adjusted = false + , workspaceWidth + , cloneLimit = this.wtTable.getRenderedRowsCount(); + + if (totalColumns > 0) { + this.adjustAvailableNodes(); + adjusted = true; + + this.renderColGroups(); + + this.renderColumnHeaders(); + + displayTds = this.wtTable.getRenderedColumnsCount(); + + //Render table rows + this.renderRows(totalRows, cloneLimit, displayTds); + + if (!this.wtTable.isWorkingOnClone()) { + workspaceWidth = this.instance.wtViewport.getWorkspaceWidth(); + this.instance.wtViewport.containerWidth = null; + } else { + this.adjustColumnHeaderHeights(); + } + + this.adjustColumnWidths(displayTds); + } + + if (!adjusted) { + this.adjustAvailableNodes(); + } + + this.removeRedundantRows(cloneLimit); + + if (!this.wtTable.isWorkingOnClone()) { + this.markOversizedRows(); + + this.instance.wtViewport.createVisibleCalculators(); + + 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; + + var firstRendered = this.wtTable.getFirstRenderedColumn(); + var lastRendered = this.wtTable.getLastRenderedColumn(); + + for (var i = firstRendered ; i < lastRendered; i++) { + var width = this.wtTable.getStretchedColumnWidth(i); + var renderedIndex = this.columnFilter.sourceToRendered(i); + this.COLGROUP.childNodes[renderedIndex + this.rowHeaderCount].style.width = width + 'px'; + } + } + + this.instance.wtScrollbars.refresh(false); + + this.instance.getSetting('onDraw', true); + } + +}; + +WalkontableTableRenderer.prototype.removeRedundantRows = function (renderedRowsCount) { + while (this.wtTable.tbodyChildrenLength > renderedRowsCount) { + this.TBODY.removeChild(this.TBODY.lastChild); + this.wtTable.tbodyChildrenLength--; + } +}; + +WalkontableTableRenderer.prototype.renderRows = function (totalRows, cloneLimit, displayTds) { + var lastTD, TR; + var visibleRowIndex = 0; + var sourceRowIndex = this.rowFilter.renderedToSource(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); + + //after last column is rendered, check if last cell is fully displayed + if (!isWorkingOnClone) { + this.resetOversizedRow(sourceRowIndex); + } + + + if (TR.firstChild) { + //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 + var height = this.instance.wtTable.getRowHeight(sourceRowIndex); + if (height) { + TR.firstChild.style.height = height + 'px'; + } + else { + TR.firstChild.style.height = ''; + } + } + + visibleRowIndex++; + + sourceRowIndex = this.rowFilter.renderedToSource(visibleRowIndex); + } +}; + +WalkontableTableRenderer.prototype.resetOversizedRow = function (sourceRow) { + if (this.instance.wtViewport.oversizedRows && this.instance.wtViewport.oversizedRows[sourceRow]) { + this.instance.wtViewport.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.renderedToSource(rowCount); + previousRowHeight = this.instance.wtTable.getRowHeight(sourceRowIndex); + currentTr = this.instance.wtTable.getTrForRow(sourceRowIndex); + + trInnerHeight = Handsontable.Dom.innerHeight(currentTr) - 1; + + if ((!previousRowHeight && this.instance.wtSettings.settings.defaultRowHeight < trInnerHeight || previousRowHeight < trInnerHeight)) { + this.instance.wtViewport.oversizedRows[sourceRowIndex] = trInnerHeight; + } + } + +}; + +WalkontableTableRenderer.prototype.adjustColumnHeaderHeights = function () { + var columnHeaders = this.instance.getSetting('columnHeaders'); + for(var i = 0, columnHeadersCount = columnHeaders.length; i < columnHeadersCount; i++) { + if(this.instance.wtViewport.oversizedColumnHeaders[i]) { + if(this.instance.wtTable.THEAD.childNodes[i].childNodes.length === 0) { + return; + } + this.instance.wtTable.THEAD.childNodes[i].childNodes[0].style.height = this.instance.wtViewport.oversizedColumnHeaders[i] + "px"; + } + } +}; + +WalkontableTableRenderer.prototype.markIfOversizedColumnHeader = function (col) { + var colCount = this.instance.wtTable.THEAD.childNodes.length !== 0 ? this.instance.wtTable.THEAD.childNodes[0].childNodes.length : 0, + sourceColIndex, + previousColHeaderHeight, + currentHeader, + currentHeaderHeight, + columnHeaders = this.instance.getSetting('columnHeaders'), + columnHeaderCount = columnHeaders.length, + level = columnHeaderCount; + + sourceColIndex = this.instance.wtTable.columnFilter.renderedToSource(col); + + while(level) { + level--; + + previousColHeaderHeight = this.instance.wtTable.getColumnHeaderHeight(level); + currentHeader = this.instance.wtTable.getColumnHeader(sourceColIndex, level); + + if(!currentHeader) { + continue; + } + + currentHeaderHeight = Handsontable.Dom.innerHeight(currentHeader) - 1; + + if ((!previousColHeaderHeight && this.instance.wtSettings.settings.defaultRowHeight < currentHeaderHeight || previousColHeaderHeight < currentHeaderHeight)) { + this.instance.wtViewport.oversizedColumnHeaders[level] = currentHeaderHeight; + } + } +}; + +WalkontableTableRenderer.prototype.renderCells = function (sourceRowIndex, TR, displayTds) { + var TD, sourceColIndex; + + for (var visibleColIndex = 0; visibleColIndex < displayTds; visibleColIndex++) { + sourceColIndex = this.columnFilter.renderedToSource(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); + } + + if (!Handsontable.Dom.hasClass(TD, 'hide')) { + TD.className = ''; + } + + TD.removeAttribute('style'); + + this.instance.wtSettings.settings.cellRenderer(sourceRowIndex, sourceColIndex, TD); + + } + + return TD; +}; + +WalkontableTableRenderer.prototype.adjustColumnWidths = function (displayTds) { + var width; + + this.instance.wtViewport.columnsRenderCalculator.refreshStretching(this.instance.wtViewport.getViewportWidth()); + + for (var renderedColIndex = 0; renderedColIndex < displayTds; renderedColIndex++) { + width = this.wtTable.getStretchedColumnWidth(this.columnFilter.renderedToSource(renderedColIndex)); + this.COLGROUP.childNodes[renderedColIndex + this.rowHeaderCount].style.width = width + 'px'; + } +}; + +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){ + TH.className = ''; + TH.removeAttribute('style'); + this.rowHeaders[col](row, TH, col); +}; + +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 () { + //adjust COLGROUP + this.adjustColGroups(); + + //adjust THEAD + this.adjustThead(); +}; + +WalkontableTableRenderer.prototype.renderColumnHeaders = function () { + if (!this.columnHeaderCount) { + return; + } + + var columnCount = this.wtTable.getRenderedColumnsCount(), + TR, + renderedColumnIndex; + + for (var i = 0; i < this.columnHeaderCount; i++) { + TR = this.getTrForColumnHeaders(i); + + for (renderedColumnIndex = (-1) * this.rowHeaderCount; renderedColumnIndex < columnCount; renderedColumnIndex++) { + var sourceCol = this.columnFilter.renderedToSource(renderedColumnIndex); + this.renderColumnHeader(i, sourceCol, TR.childNodes[renderedColumnIndex + this.rowHeaderCount]); + + if(!this.wtTable.isWorkingOnClone()) { + this.markIfOversizedColumnHeader(renderedColumnIndex); + } + } + } +}; + +WalkontableTableRenderer.prototype.adjustColGroups = function () { + var columnCount = this.wtTable.getRenderedColumnsCount(); + + //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--; + } +}; + +WalkontableTableRenderer.prototype.adjustThead = function () { + var columnCount = this.wtTable.getRenderedColumnsCount(); + var TR = this.THEAD.firstChild; + if (this.columnHeaders.length) { + + for (var i = 0, columnHeadersLength = this.columnHeaders.length; i < columnHeadersLength; i++) { + TR = this.THEAD.childNodes[i]; + 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--; + } + } + + var theadChildrenLength = this.THEAD.childNodes.length; + if(theadChildrenLength > this.columnHeaders.length) { + for(var i = this.columnHeaders.length; i < theadChildrenLength; i++ ) { + this.THEAD.removeChild(this.THEAD.lastChild); + } + } + } + + else if (TR) { + Handsontable.Dom.empty(TR); + } +}; + +WalkontableTableRenderer.prototype.getTrForColumnHeaders = function (index) { + var TR = this.THEAD.childNodes[index]; + return TR; +}; + +WalkontableTableRenderer.prototype.renderColumnHeader = function (row, col, TH) { + TH.className = ''; + TH.removeAttribute('style'); + return this.columnHeaders[row](col, TH, row); +}; + +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.removeRedundantColumns = function (renderedColumnsCount) { + while (this.wtTable.tbodyChildrenLength > renderedColumnsCount) { + this.TBODY.removeChild(this.TBODY.lastChild); + this.wtTable.tbodyChildrenLength--; + } +}; + +/* + 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.oversizedRows = []; + this.oversizedCols = []; + this.oversizedColumnHeaders = []; + + var that = this; + + var eventManager = Handsontable.eventManager(instance); + eventManager.addEventListener(window,'resize',function () { + that.clientHeight = that.getWorkspaceHeight(); + }); +} + +WalkontableViewport.prototype.getWorkspaceHeight = function () { + var scrollHandler = this.instance.wtScrollbars.vertical.scrollHandler; + if (scrollHandler === window) { + return document.documentElement.clientHeight; + } + else { + var elemHeight = Handsontable.Dom.outerHeight(scrollHandler); + var height = (elemHeight > 0 && scrollHandler.clientHeight > 0) ? scrollHandler.clientHeight : Infinity; //returns height without DIV scrollbar + return height; + } +}; + + +WalkontableViewport.prototype.getWorkspaceWidth = function () { + var width, + totalColumns = this.instance.getSetting("totalColumns"), + scrollHandler = this.instance.wtScrollbars.horizontal.scrollHandler, + overflow, + stretchSetting = this.instance.getSetting('stretchH'); + + if(Handsontable.freezeOverlays) { + width = Math.min(document.documentElement.offsetWidth - this.getWorkspaceOffset().left, document.documentElement.offsetWidth); + } else { + width = Math.min(this.getContainerFillWidth(), document.documentElement.offsetWidth - this.getWorkspaceOffset().left, document.documentElement.offsetWidth); + } + + if (scrollHandler === window && totalColumns > 0 && this.sumColumnWidths(0, totalColumns - 1) > width) { + //in case sum of column widths is higher than available stylesheet width, let's assume using the whole window + //otherwise continue below, which will allow stretching + //this is used in `scroll_window.html` + //TODO test me + return document.documentElement.clientWidth; + } + + if (scrollHandler !== window){ + overflow = this.instance.wtScrollbars.horizontal.scrollHandler.style.overflow; + + if (overflow == "scroll" || overflow == "hidden" || overflow == "auto") { + //this is used in `scroll.html` + //TODO test me + return Math.max(width, scrollHandler.clientWidth); + } + } + + if(stretchSetting === 'none' || !stretchSetting) { + // if no stretching is used, return the maximum used workspace width + return Math.max(width, Handsontable.Dom.outerWidth(this.instance.wtTable.TABLE)); + } else { + // if stretching is used, return the actual container width, so the columns can fit inside it + return width; + } +}; + +WalkontableViewport.prototype.sumColumnWidths = function (from, length) { + var sum = 0; + while(from < length) { + sum += this.instance.wtTable.getColumnWidth(from) || this.instance.wtSettings.defaultColumnWidth; + from++; + } + return sum; +}; +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
t
t
element corresponding to params row, col + * @param {Number} row + * @param {Number} col + * @param {Boolean} topmost + * @public + * @return {Element} + */ + this.getCell = function (row, col, topmost) { + return instance.view.getCellAtCoords(new WalkontableCellCoords(row, col), topmost); + }; + + /** + * Returns coordinates for the provided element + * @param elem + * @returns {WalkontableCellCoords|*} + */ + this.getCoords = function(elem) { + return this.view.wt.wtTable.getCoords.call(this.view.wt.wtTable, elem); + }; + + /** + * 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 = [], + range; + + range = datamap.getRange( + new WalkontableCellCoords(0, datamap.propToCol(prop)), + new WalkontableCellCoords(priv.settings.data.length - 1, datamap.propToCol(prop)), + datamap.DESTINATION_RENDERER); + + return out.concat.apply(out, range); + }; + + /** + * 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); + /* jshint ignore:start */ + if(cellMeta[key] != undefined){ + delete priv.cellSettings[row][col][key]; + } + /* jshint ignore:end */ + }; + + /** + * 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') { + /* jshint -W089 */ + for (var key in prop) { + var value = prop[key]; + 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.run(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){ + // warning: this must be done after datamap.colToProp + return Handsontable.hooks.run(instance, 'modifyCol', col); + } + + 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; + + /* jshint ignore:start */ + 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--; + } + /* jshint ignore:end */ + 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 (Array.isArray(priv.settings.rowHeaders) && 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.run(instance, 'modifyCol', col); + + if (priv.settings.columns && priv.settings.columns[col] && priv.settings.columns[col].title) { + return priv.settings.columns[col].title; + } + else if (Array.isArray(priv.settings.colHeaders) && 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.run(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); + + height = Handsontable.hooks.run(instance, 'modifyRowHeight', height, row); + + return height; + }; + + /** + * 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 rendered row + * @return {Number} + */ + this.rowOffset = function () { + return instance.view.wt.wtTable.getFirstRenderedRow(); + }; + + /** + * Return index of first visible column + * @return {Number} + */ + this.colOffset = function () { + return instance.view.wt.wtTable.getFirstRenderedColumn(); + }; + + /** + * Return number of rendered rows (including rows partially or fully rendered outside viewport). Returns -1 if table is not visible + * @return {Number} + */ + this.countRenderedRows = function () { + return instance.view.wt.drawn ? instance.view.wt.wtTable.getRenderedRowsCount() : -1; + }; + + /** + * Return number of visible rows (rendered rows that fully fit inside viewport)). Returns -1 if table is not visible + * @return {Number} + */ + this.countVisibleRows = function () { + return instance.view.wt.drawn ? instance.view.wt.wtTable.getVisibleRowsCount() : -1; + }; + + /** + * Return number of rendered columns (including columns partially or fully rendered outside viewport). Returns -1 if table is not visible + * @return {Number} + */ + this.countRenderedCols = function () { + return instance.view.wt.drawn ? instance.view.wt.wtTable.getRenderedColumnsCount() : -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.getVisibleColumnsCount() : - 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.run(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) { + /* jshint ignore:start */ + arguments[1] = datamap.propToCol(arguments[1]); + if (typeof arguments[3] !== "undefined") { + arguments[3] = datamap.propToCol(arguments[3]); + } + return instance.selectCell.apply(instance, arguments); + /* jshint ignore:end */ + }; + + /** + * 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.destroy(); + } + + + Handsontable.Dom.empty(instance.rootElement); + eventManager.clear(); + + 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") { + 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; + }; + + /** + * 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) { + return Handsontable.hooks.run(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) { + return elem; + } + elem = elem.parentNode; + } + return null; +}; + +/** + * Goes up the DOM tree and checks if element is child of another element + * @param child Child element + * @param {Object|string} parent Parent element OR selector of the parent element. If classname provided, function returns true for the first occurance of element with that class. + * @returns {boolean} + */ +Handsontable.Dom.isChildOf = function (child, parent) { + var node = child.parentNode; + var queriedParents = []; + if(typeof parent === "string") { + queriedParents = Array.prototype.slice.call(document.querySelectorAll(parent), 0); + } else { + queriedParents.push(parent); + } + + while (node != null) { + if (queriedParents.indexOf(node) > - 1) { + 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; + if (elem.previousSibling) { + /* jshint ignore:start */ + while (elem = elem.previousSibling) { + ++i; + } + /* jshint ignore:end */ + } + 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) { + if (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 + } + }; +} + +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; + /* jshint ignore:start */ + while (child = element.lastChild) { + element.removeChild(child); + } + /* jshint ignore:end */ +}; + +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 if element is attached to the DOM and visible, false otherwise + * @param elem + * @returns {boolean} + */ +Handsontable.Dom.isVisible = function (elem) { + 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. Function is not compatible with jQuery offset. + * + * @param {HTMLElement} elem + * @return {Object} Returns object with `top` and `left` props + */ +Handsontable.Dom.offset = function (elem) { + var offsetLeft, + offsetTop, + lastElem, + docElem, + box; + + docElem = document.documentElement; + + 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 + box = elem.getBoundingClientRect(); + + return { + top: box.top + (window.pageYOffset || docElem.scrollTop) - (docElem.clientTop || 0), + left: box.left + (window.pageXOffset || docElem.scrollLeft) - (docElem.clientLeft || 0) + }; + } + offsetLeft = elem.offsetLeft; + offsetTop = elem.offsetTop; + lastElem = elem; + + /* jshint ignore:start */ + while (elem = elem.offsetParent) { + // from my observation, document.body always has scrollLeft/scrollTop == 0 + if (elem === document.body) { + break; + } + offsetLeft += elem.offsetLeft; + offsetTop += elem.offsetTop; + lastElem = elem; + } + /* jshint ignore:end */ + + //slow - http://jsperf.com/offset-vs-getboundingclientrect/6 + if (lastElem && lastElem.style.position === 'fixed') { + //if(lastElem !== document.body) { //faster but does gives false positive in Firefox + offsetLeft += window.pageXOffset || docElem.scrollLeft; + offsetTop += window.pageYOffset || docElem.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.getScrollableElement = function (element) { + var el = element.parentNode, + props = ['auto', 'scroll'], + overflow, overflowX, overflowY; + + while (el && el.style) { + overflow = el.style.overflow; + overflowX = el.style.overflowX; + overflowY = el.style.overflowY; + + if (overflow == 'scroll' || overflowX == 'scroll' || overflowY == 'scroll') { + return el; + } + if (el.clientHeight < el.scrollHeight && (props.indexOf(overflowY) !== -1 || props.indexOf(overflow) !== -1)) { + return el; + } + if (el.clientWidth < el.scrollWidth && (props.indexOf(overflowX) !== -1 || props.indexOf(overflow) !== -1)) { + return el; + } + el = el.parentNode; + } + + return window; +}; + +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 || elem.innerHeight; +}; + +Handsontable.Dom.innerWidth = function (elem) { + return elem.clientWidth || elem.innerWidth; +}; + +Handsontable.Dom.addEvent = function(element, event, callback) { + if (window.addEventListener) { + element.addEventListener(event, callback, false); + } else { + element.attachEvent('on' + event, callback); + } +}; + +Handsontable.Dom.removeEvent = function(element, event, callback) { + if (window.removeEventListener) { + element.removeEventListener(event, callback, false); + } else { + element.detachEvent('on' + event, callback); + } +}; + + +(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)) { + this.columnHeaderHeight = Handsontable.Dom.outerHeight(this.instance.wtTable.THEAD); + } + return this.columnHeaderHeight; +}; + +WalkontableViewport.prototype.getViewportHeight = function () { + + var containerHeight = this.getWorkspaceHeight(); + + 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 () { + var containerWidth = this.getWorkspaceWidth(), + rowHeaderWidth; + + if (containerWidth === Infinity) { + return containerWidth; + } + rowHeaderWidth = this.getRowHeaderWidth(); + + if (rowHeaderWidth > 0) { + return containerWidth - rowHeaderWidth; + } + + return containerWidth; +}; + +/** + * Creates: + * - rowsRenderCalculator (before draw, to qualify rows for rendering) + * - rowsVisibleCalculator (after draw, to measure which rows are actually visible) + * @returns {WalkontableViewportRowsCalculator} + */ +WalkontableViewport.prototype.createRowsCalculator = function (visible) { + this.rowHeaderWidth = NaN; + + var height; + if (this.instance.wtSettings.settings.renderAllRows) { + height = Infinity; + } + else { + height = this.getViewportHeight(); + } + + var pos = this.instance.wtScrollbars.vertical.getScrollPosition() - this.instance.wtScrollbars.vertical.getTableParentOffset(); + if (pos < 0) { + pos = 0; + } + + var fixedRowsTop = this.instance.getSetting('fixedRowsTop'); + if(fixedRowsTop) { + var fixedRowsHeight = this.instance.wtScrollbars.vertical.sumCellSizes(0, fixedRowsTop); + pos += fixedRowsHeight; + height -= fixedRowsHeight; + } + + var that = this; + return new WalkontableViewportRowsCalculator( + height, + pos, + this.instance.getSetting('totalRows'), + function(sourceRow) { + return that.instance.wtTable.getRowHeight(sourceRow); + }, + visible ? null : this.instance.wtSettings.settings.viewportRowCalculatorOverride, + visible ? true : false + ); +}; + +/** + * Creates: + * - columnsRenderCalculator (before draw, to qualify columns for rendering) + * - columnsVisibleCalculator (after draw, to measure which columns are actually visible) + * @returns {WalkontableViewportRowsCalculator} + */ +WalkontableViewport.prototype.createColumnsCalculator = function (visible) { + this.columnHeaderHeight = NaN; + + var width = this.getViewportWidth(); + + var pos = this.instance.wtScrollbars.horizontal.getScrollPosition() - this.instance.wtScrollbars.vertical.getTableParentOffset(); + if (pos < 0) { + pos = 0; + } + + var fixedColumnsLeft = this.instance.getSetting('fixedColumnsLeft'); + if(fixedColumnsLeft) { + var fixedColumnsWidth = this.instance.wtScrollbars.horizontal.sumCellSizes(0, fixedColumnsLeft); + pos += fixedColumnsWidth; + width -= fixedColumnsWidth; + } + + var that = this; + return new WalkontableViewportColumnsCalculator( + width, + pos, + this.instance.getSetting('totalColumns'), + function (sourceCol) { + return that.instance.wtTable.getColumnWidth(sourceCol); + }, + visible ? null : this.instance.wtSettings.settings.viewportColumnCalculatorOverride, + visible ? true : false, + this.instance.getSetting('stretchH') + ); +}; + + +/** + * Creates rowsRenderCalculator and columnsRenderCalculator (before draw, to determine what rows and cols should be rendered) + * @param fastDraw {Boolean} If TRUE, will try to avoid full redraw and only update the border positions. If FALSE or UNDEFINED, will perform a full redraw + */ +WalkontableViewport.prototype.createRenderCalculators = function (fastDraw) { + if (fastDraw) { + var proposedRowsVisibleCalculator = this.createRowsCalculator(true); + var proposedColumnsVisibleCalculator = this.createColumnsCalculator(true); + if (!(this.areAllProposedVisibleRowsAlreadyRendered(proposedRowsVisibleCalculator) && this.areAllProposedVisibleColumnsAlreadyRendered(proposedColumnsVisibleCalculator) ) ) { + fastDraw = false; + } + } + + if(!fastDraw) { + this.rowsRenderCalculator = this.createRowsCalculator(); + this.columnsRenderCalculator = this.createColumnsCalculator(); + } + + this.rowsVisibleCalculator = null; //delete temporarily to make sure that renderers always use rowsRenderCalculator, not rowsVisibleCalculator + this.columnsVisibleCalculator = null; + + return fastDraw; +}; + +/** + * Creates rowsVisibleCalculator and columnsVisibleCalculator (after draw, to determine what are the actually visible rows and columns) + */ +WalkontableViewport.prototype.createVisibleCalculators = function () { + this.rowsVisibleCalculator = this.createRowsCalculator(true); + this.columnsVisibleCalculator = this.createColumnsCalculator(true); +}; + +/** + * Returns information whether proposedRowsVisibleCalculator viewport + * is contained inside rows rendered in previous draw (cached in rowsRenderCalculator) + * + * Returns TRUE if all proposed visible rows are already rendered (meaning: redraw is not needed) + * Returns FALSE if at least one proposed visible row is not already rendered (meaning: redraw is needed) + * + * @returns {boolean} + */ +WalkontableViewport.prototype.areAllProposedVisibleRowsAlreadyRendered = function (proposedRowsVisibleCalculator) { + if (this.rowsVisibleCalculator) { + if (proposedRowsVisibleCalculator.startRow < this.rowsRenderCalculator.startRow || + (proposedRowsVisibleCalculator.startRow === this.rowsRenderCalculator.startRow && + proposedRowsVisibleCalculator.startRow > 0)) { + return false; + } + else if (proposedRowsVisibleCalculator.endRow > this.rowsRenderCalculator.endRow || + (proposedRowsVisibleCalculator.endRow === this.rowsRenderCalculator.endRow && + proposedRowsVisibleCalculator.endRow < this.instance.getSetting('totalRows') - 1)) { + return false; + } + else { + return true; + } + } + return false; +}; + +/** + * Returns information whether proposedColumnsVisibleCalculator viewport + * is contained inside column rendered in previous draw (cached in columnsRenderCalculator) + * + * Returns TRUE if all proposed visible columns are already rendered (meaning: redraw is not needed) + * Returns FALSE if at least one proposed visible column is not already rendered (meaning: redraw is needed) + * + * @returns {boolean} + */ +WalkontableViewport.prototype.areAllProposedVisibleColumnsAlreadyRendered = function (proposedColumnsVisibleCalculator) { + if (this.columnsVisibleCalculator) { + if (proposedColumnsVisibleCalculator.startColumn < this.columnsRenderCalculator.startColumn || + (proposedColumnsVisibleCalculator.startColumn === this.columnsRenderCalculator.startColumn && + proposedColumnsVisibleCalculator.startColumn > 0)) { + return false; + } + else if (proposedColumnsVisibleCalculator.endColumn > this.columnsRenderCalculator.endColumn || + (proposedColumnsVisibleCalculator.endColumn === this.columnsRenderCalculator.endColumn && + proposedColumnsVisibleCalculator.endColumn < this.instance.getSetting('totalColumns') - 1)) { + return false; + } + else { + return true; + } + } + return false; +}; + +function WalkontableViewportColumnsCalculator (width, scrollOffset, totalColumns, columnWidthFn, overrideFn, onlyFullyVisible, stretchH) { + var + _this = this, + ratio = 1, + sum = 0, + needReverse = true, + defaultColumnWidth = 50, + startPositions = [], + getColumnWidth, + columnWidth, i; + + this.scrollOffset = scrollOffset; + this.startColumn = null; + this.endColumn = null; + this.startPosition = null; + this.count = 0; + this.stretchAllRatio = 0; + this.stretchLastWidth = 0; + this.stretch = stretchH; + this.totalTargetWidth = 0; + this.needVerifyLastColumnWidth = true; + this.stretchAllColumnsWidth = []; + + + function getStretchedAllColumnWidth(column, baseWidth) { + var sumRatioWidth = 0; + + if (!_this.stretchAllColumnsWidth[column]) { + _this.stretchAllColumnsWidth[column] = Math.round(baseWidth * _this.stretchAllRatio); + } + if (_this.stretchAllColumnsWidth.length === totalColumns && _this.needVerifyLastColumnWidth) { + _this.needVerifyLastColumnWidth = false; + + for (var i = 0; i < _this.stretchAllColumnsWidth.length; i++) { + sumRatioWidth += _this.stretchAllColumnsWidth[i]; + } + if (sumRatioWidth != _this.totalTargetWidth) { + _this.stretchAllColumnsWidth[_this.stretchAllColumnsWidth.length - 1] += _this.totalTargetWidth - sumRatioWidth; + } + } + + return _this.stretchAllColumnsWidth[column]; + } + + function getStretchedLastColumnWidth(column, baseWidth) { + if (column === totalColumns - 1) { + return _this.stretchLastWidth; + } + + return null; + } + + getColumnWidth = function getColumnWidth(i) { + var width = columnWidthFn(i); + + ratio = ratio || 1; + + if (width === undefined) { + width = defaultColumnWidth; + } + + return width; + }; + + /** + * Recalculate columns stretching. + * + * @param {Number} totalWidth + */ + this.refreshStretching = function (totalWidth) { + var sumAll = 0, + columnWidth, + remainingSize; + + for (var i = 0; i < totalColumns; i++) { + columnWidth = getColumnWidth(i); + sumAll += columnWidth; + } + this.totalTargetWidth = totalWidth; + remainingSize = sumAll - totalWidth; + + if (this.stretch === 'all' && remainingSize < 0) { + this.stretchAllRatio = totalWidth / sumAll; + this.stretchAllColumnsWidth = []; + this.needVerifyLastColumnWidth = true; + } + else if (this.stretch === 'last' && totalWidth !== Infinity) { + this.stretchLastWidth = -remainingSize + getColumnWidth(totalColumns - 1); + } + }; + + /** + * Get stretched column width based on stretchH (all or last) setting passed in handsontable instance. + * + * @param {Number} column + * @param {Number} baseWidth + * @returns {Number|null} + */ + this.getStretchedColumnWidth = function(column, baseWidth) { + var result = null; + + if (this.stretch === 'all' && this.stretchAllRatio !== 0) { + result = getStretchedAllColumnWidth(column, baseWidth); + } + else if (this.stretch === 'last' && this.stretchLastWidth !== 0) { + result = getStretchedLastColumnWidth(column, baseWidth); + } + + return result; + }; + + + for (i = 0; i < totalColumns; i++) { + columnWidth = getColumnWidth(i); + + if (sum <= scrollOffset && !onlyFullyVisible) { + this.startColumn = i; + } + + if (sum >= scrollOffset && sum + columnWidth <= scrollOffset + width) { + if (this.startColumn == null) { + this.startColumn = i; + } + this.endColumn = i; + } + startPositions.push(sum); + sum += columnWidth; + + if (!onlyFullyVisible) { + this.endColumn = i; + } + if (sum >= scrollOffset + width) { + needReverse = false; + break; + } + } + + if (this.endColumn == totalColumns - 1 && needReverse) { + this.startColumn = this.endColumn; + + while (this.startColumn > 0) { + var viewportSum = startPositions[this.endColumn] + columnWidth - startPositions[this.startColumn - 1]; + + if (viewportSum <= width || !onlyFullyVisible) { + this.startColumn--; + } + if (viewportSum > width) { + break; + } + } + } + + if (this.startColumn !== null && overrideFn) { + overrideFn(this); + } + this.startPosition = startPositions[this.startColumn]; + + if (this.startPosition == void 0) { + this.startPosition = null; + } + if (this.startColumn != null) { + this.count = this.endColumn - this.startColumn + 1; + } +} + + +/** + * Viewport calculator constructor. Calculates indexes of rows to render OR rows that are visible. + * To redo the calculation, you need to create a new calculator. + * + * Object properties: + * this.scrollOffset - position of vertical scroll (in px) + * this.startRow - index of the first rendered/visible row (can be overwritten using overrideFn) + * this.startPosition - position of the first rendered/visible row (in px) + * this.endRow - index of the last rendered/visible row (can be overwritten using overrideFn) + * this.count - number of rendered/visible rows + * + * @param height - height of the viewport + * @param scrollOffset - current vertical scroll position of the viewport + * @param totalRows - total number of rows + * @param rowHeightFn - function that returns the height of the row at a given index (in px) + * @param overrideFn - function that changes calculated this.startRow, this.endRow (used by mergeCells.js plugin) + * @param onlyFullyVisible {bool} - if TRUE, only startRow and endRow will be indexes of rows that are FULLY in viewport + * @constructor + */ +function WalkontableViewportRowsCalculator(height, scrollOffset, totalRows, rowHeightFn, overrideFn, onlyFullyVisible) { + this.scrollOffset = scrollOffset; + this.startRow = null; + this.startPosition = null; + this.endRow = null; + this.count = 0; + var sum = 0; + var rowHeight; + var needReverse = true; + var defaultRowHeight = 23; + var startPositions = []; + for (var i = 0; i < totalRows; i++) { + rowHeight = rowHeightFn(i); + if (rowHeight === undefined) { + rowHeight = defaultRowHeight; + } + if (sum <= scrollOffset && !onlyFullyVisible) { + this.startRow = i; + } + if (sum >= scrollOffset && sum + rowHeight <= scrollOffset + height) { + if (this.startRow == null) { + this.startRow = i; + } + this.endRow = i; + } + startPositions.push(sum); + sum += rowHeight; + if(!onlyFullyVisible) { + this.endRow = i; + } + if (sum >= scrollOffset + height) { + needReverse = false; + break; + } + } + + //If the rendering has reached the last row and there is still some space available in the viewport, we need to render in reverse in order to fill the whole viewport with rows + if (this.endRow == totalRows - 1 && needReverse) { + this.startRow = this.endRow; + while(this.startRow > 0) { + var viewportSum = startPositions[this.endRow] + rowHeight - startPositions[this.startRow - 1]; //rowHeight is the height of the last row + if (viewportSum <= height || !onlyFullyVisible) + { + this.startRow--; + } + if (viewportSum >= height) + { + break; + } + } + } + + if (this.startRow !== null && overrideFn) { + overrideFn(this); + } + + this.startPosition = startPositions[this.startRow]; + if (this.startPosition == void 0) { + this.startPosition = null; + } + + if (this.startRow != null) { + this.count = this.endRow - this.startRow + 1; + } +} + +if (window.jQuery) { + (function (window, $, Handsontable) { + $.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[0], 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); + + if (action === 'destroy'){ + $this.removeData(); + } + } + else { + throw new Error('Handsontable do not provide action: ' + action); + } + } + + return output; + } + }; + })(window, jQuery, Handsontable); +} + + + +})(window, Handsontable); diff --git a/bower_components/handsontable/handsontable.jquery.json b/bower_components/handsontable/handsontable.jquery.json new file mode 100644 index 0000000..3677770 --- /dev/null +++ b/bower_components/handsontable/handsontable.jquery.json @@ -0,0 +1,37 @@ +{ + "name": "handsontable", + "title": "Handsontable", + "version": "0.12.4", + "author": { + "name": "Marcin Warpechowski", + "email": "hello@handsontable.com", + "url": "http://www.nextgen.pl/" + }, + "licenses": [ + { + "type": "MIT", + "url": "https://raw.github.com/handsontable/handsontable/master/LICENSE" + } + ], + "description": "Spreadsheet-like data grid editor that provides copy/paste functionality compatible with Excel/Google Docs", + "homepage": "http://handsontable.com/", + "docs": "https://github.com/handsontable/handsontable", + "demo": "http://handsontable.com/", + "download": "https://github.com/handsontable/handsontable/archive/master.zip", + "keywords": [ + "grid", + "datagrid", + "table", + "ui", + "input", + "ajax", + "handsontable", + "spreadsheet" + ], + "bugs": "https://github.com/handsontable/handsontable/issues", + "dependencies": { + "jquery": ">=1.7", + "bootstrap-typeahead.js": "2.1.1", + "contextMenu": ">=1.5.25" + } +} diff --git a/bower_components/handsontable/index.html b/bower_components/handsontable/index.html new file mode 100644 index 0000000..564e0ba --- /dev/null +++ b/bower_components/handsontable/index.html @@ -0,0 +1,416 @@ + + + + + Handsontable - JavaScript data grid editor. Excel-like grid editing with HTML & JavaScript + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+ + + + Star + + + + + + + + Fork + + + +
+ +
+
+ +
+
+
+
+ This page has been moved to + http://handsontable.com/. Please update your bookmarks. +
+
+ +
+ +

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

+ +
+ This is Handsontable , a + release published on Jan 23, 2015. + +

The most prominent changes are:

+ +
    +
  • more efficient row and column rendering algorithm,
  • +
  • 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),
  • +
  • basic iPad 4 support
  • +
  • manual column freeze functionality
  • +
+ + Check out the full release + notes. If you experience some rough edges, please report an + issue or temporarily stick to + version 0.10.5 . +
+ +
+
+
+
+
+ + + +
+ + + +
+
+ + + + +
+
+ +
+
+ + + + + diff --git a/bower_components/handsontable/package.json b/bower_components/handsontable/package.json new file mode 100644 index 0000000..11146d6 --- /dev/null +++ b/bower_components/handsontable/package.json @@ -0,0 +1,28 @@ +{ + "name": "handsontable", + "description": "Spreadsheet-like data grid editor that provides copy/paste functionality compatible with Excel/Google Docs", + "homepage": "http://handsontable.com/", + "bugs": { + "url": "https://github.com/handsontable/handsontable/issues" + }, + "author": "Marcin Warpechowski ", + "version": "0.12.4", + "devDependencies": { + "grunt": "~0.4.5", + "grunt-replace": "~0.7.8", + "grunt-contrib-clean": "~0.4.1", + "grunt-contrib-concat": "~0.5.0", + "grunt-contrib-watch": "~0.6.1", + "grunt-contrib-jasmine": "~0.5.1", + "grunt-contrib-connect": "~0.8.0", + "grunt-contrib-uglify": "~0.5.1", + "grunt-contrib-cssmin": "~0.10.0", + "grunt-saucelabs": "~8.3.3", + "grunt-gitinfo": "~0.1.6", + "grunt-contrib-jshint": "~0.10.0", + "time-grunt": "~1.0.0" + }, + "scripts": { + "test": "grunt test -v" + } +} diff --git a/bower_components/handsontable/sauce_connect.log b/bower_components/handsontable/sauce_connect.log new file mode 100644 index 0000000..61ffc8b --- /dev/null +++ b/bower_components/handsontable/sauce_connect.log @@ -0,0 +1,26 @@ +2014-11-27 11:33:10,796 - sauce_connect:558 - INFO - / Starting \ +2014-11-27 11:33:10,802 - sauce_connect:559 - INFO - Please wait for "You may start your tests" to start your tests. +2014-11-27 11:33:10,809 - sauce_connect:571 - DEBUG - System is 2.0 hours off UTC +2014-11-27 11:33:10,811 - sauce_connect:573 - DEBUG - options: {'user': 'warpech', 'ports': ['53209'], 'domains': None, 'debug_ssh': False, 'tunnel_ports': ['80'], 'allow_unclean_exit': False, 'rest_url': 'https://saucelabs.com/rest/v1', 'logfile': 'sauce_connect.log', 'squid_opts': '', 'ssh': False, 'se_port': '4445', 'readyfile': None, 'shared_tunnel': False, 'boost_mode': True, 'tunnel_identifier': '186316376', 'ssh_port': 443, 'fast_fail_regexps': '', 'direct_domains': '', 'host': '127.0.0.1', 'quiet': False, 'latency_log': 150, 'use_ssh_config': False} +2014-11-27 11:33:10,812 - sauce_connect:574 - DEBUG - metadata: {'PythonVersion': '2.5.1', 'OwnerHost': '127.0.0.1', 'Release': '3.0-r27', 'OwnerPorts': ['53209'], 'Ports': ['80'], 'Platform': 'Java-1.6.0_65-Java_HotSpot-TM-_64-Bit_Server_VM,_20.65-b04-462,_Apple_Inc.-on-Mac_OS_X-10.9.5-x86_64', 'Build': '42', 'ScriptRelease': 42, 'ScriptName': 'sauce_connect'} +2014-11-27 11:33:10,813 - sauce_connect:576 - INFO - Forwarding: None:['80'] -> 127.0.0.1:['53209'] +2014-11-27 11:33:10,828 - sauce_connect:375 - INFO - Succesfully connected to local server 127.0.0.1:53209 in 7ms +2014-11-27 11:33:14,921 - sauce_connect:238 - INFO - {"squid_config":null,"use_caching_proxy":true,"metadata":{"PythonVersion":"2.5.1","OwnerHost":"127.0.0.1","Release":"3.0-r27","OwnerPorts":["53209"],"Ports":["80"],"Platform":"Java-1.6.0_65-Java_HotSpot-TM-_64-Bit_Server_VM,_20.65-b04-462,_Apple_Inc.-on-Mac_OS_X-10.9.5-x86_64","Build":"42","ScriptRelease":42,"ScriptName":"sauce_connect"},"use_kgp":true,"tunnel_identifier":"186316376","shared_tunnel":false,"fast_fail_regexps":null,"ssh_port":443,"direct_domains":null,"domain_names":[]} +2014-11-27 11:33:19,176 - sauce_connect:250 - INFO - Tunnel remote VM is provisioned (2a011f0942a24403bc2eec0354be1ed3) +2014-11-27 11:33:20,507 - sauce_connect:268 - INFO - Tunnel remote VM is booting .. +2014-11-27 11:33:31,566 - sauce_connect:272 - INFO - Tunnel remote VM is running at maki76167.miso.saucelabs.com +2014-11-27 11:33:31,594 - sauce_connect:375 - INFO - Succesfully connected to local server 127.0.0.1:53209 in 0ms +2014-11-27 11:33:31,596 - sauce_connect:646 - INFO - Starting connection to tunnel host... +2014-11-27 11:33:31,598 - sauce_connect:646 - INFO - Connecting to tunnel host maki76167.miso.saucelabs.com as warpech +2014-11-27 11:33:31,769 - sauce_connect:646 - INFO - Forwarding Selenium with ephemeral port 53214 +2014-11-27 11:33:31,778 - sauce_connect:646 - INFO - Selenium HTTP proxy listening on port 4445 +2014-11-27 11:33:32,914 - sauce_connect:0 - INFO - Successful handshake with Sauce Connect server +2014-11-27 11:33:32,923 - sauce_connect:0 - INFO - Tunnel host version: 0.1.0, remote endpoint ID: bb0bc29ff93347dea7e4d17a56b82db7 +2014-11-27 11:33:32,927 - sauce_connect:646 - INFO - Connected! You may start your tests. +2014-11-27 11:33:40,378 - sauce_connect:0 - INFO - Request started: GET http://localhost:1563/ +2014-11-27 11:33:40,395 - sauce_connect:0 - INFO - Could not proxy http://localhost:1563/, exception: java.net.ConnectException: Connection refused +2014-11-27 11:33:41,012 - sauce_connect:0 - INFO - Request started: GET http://localhost:38027/ +2014-11-27 11:33:41,016 - sauce_connect:0 - INFO - Could not proxy http://localhost:38027/, exception: java.net.ConnectException: Connection refused +2014-11-27 11:33:41,196 - sauce_connect:0 - INFO - Request started: GET http://localhost:37233/ +2014-11-27 11:33:41,198 - sauce_connect:0 - INFO - Could not proxy http://localhost:37233/, exception: java.net.ConnectException: Connection refused +2014-11-27 11:33:43,796 - sauce_connect:0 - INFO - received SIGTERM diff --git a/bower_components/handsontable/update.json b/bower_components/handsontable/update.json new file mode 100644 index 0000000..3876dbe --- /dev/null +++ b/bower_components/handsontable/update.json @@ -0,0 +1,10 @@ +{ + "COMMENT": "This is a update.json file used by jsDelivr.com CDN", + "packageManager": "github", + "name": "handsontable", + "repo": "handsontable/handsontable", + "files": { + "include": ["./dist/*.js", "./plugins/**/*.js", "./plugins/**/*.css"], + "exclude": ["./plugins/**/*spec*"] + } +} diff --git a/bower_components/zeroclipboard/.bower.json b/bower_components/zeroclipboard/.bower.json new file mode 100644 index 0000000..d49c562 --- /dev/null +++ b/bower_components/zeroclipboard/.bower.json @@ -0,0 +1,52 @@ +{ + "name": "zeroclipboard", + "description": "The ZeroClipboard library provides an easy way to copy text to the clipboard using an invisible Adobe Flash movie and a JavaScript interface.", + "version": "2.2.0", + "main": [ + "dist/ZeroClipboard.js", + "dist/ZeroClipboard.swf" + ], + "keywords": [ + "flash", + "clipboard", + "copy", + "cut", + "paste", + "zclip", + "clip", + "clippy" + ], + "license": "https://github.com/zeroclipboard/zeroclipboard/blob/master/LICENSE", + "authors": [ + { + "name": "Jon Rohan", + "url": "http://jonrohan.me/" + }, + { + "name": "James M. Greene", + "email": "james.m.greene@gmail.com", + "url": "http://greene.io/" + } + ], + "homepage": "http://zeroclipboard.org/", + "repository": { + "type": "git", + "url": "https://github.com/zeroclipboard/zeroclipboard.git" + }, + "location": "git://github.com/zeroclipboard/zeroclipboard.git", + "ignore": [ + "*", + "/dist/.jshintrc", + "!/bower.json", + "!/dist/**" + ], + "_release": "2.2.0", + "_resolution": { + "type": "version", + "tag": "v2.2.0", + "commit": "039fb799c916d3730fbc898c2f5ef24581c7e9b7" + }, + "_source": "git://github.com/zeroclipboard/zeroclipboard.git", + "_target": ">=2.1.6", + "_originalSource": "zeroclipboard" +} \ No newline at end of file diff --git a/bower_components/zeroclipboard/bower.json b/bower_components/zeroclipboard/bower.json new file mode 100644 index 0000000..16beecb --- /dev/null +++ b/bower_components/zeroclipboard/bower.json @@ -0,0 +1,43 @@ +{ + "name": "zeroclipboard", + "description": "The ZeroClipboard library provides an easy way to copy text to the clipboard using an invisible Adobe Flash movie and a JavaScript interface.", + "version": "2.2.0", + "main": [ + "dist/ZeroClipboard.js", + "dist/ZeroClipboard.swf" + ], + "keywords": [ + "flash", + "clipboard", + "copy", + "cut", + "paste", + "zclip", + "clip", + "clippy" + ], + "license": "https://github.com/zeroclipboard/zeroclipboard/blob/master/LICENSE", + "authors": [ + { + "name": "Jon Rohan", + "url": "http://jonrohan.me/" + }, + { + "name": "James M. Greene", + "email": "james.m.greene@gmail.com", + "url": "http://greene.io/" + } + ], + "homepage": "http://zeroclipboard.org/", + "repository": { + "type": "git", + "url": "https://github.com/zeroclipboard/zeroclipboard.git" + }, + "location": "git://github.com/zeroclipboard/zeroclipboard.git", + "ignore": [ + "*", + "/dist/.jshintrc", + "!/bower.json", + "!/dist/**" + ] +} \ No newline at end of file diff --git a/bower_components/zeroclipboard/dist/.jshintrc b/bower_components/zeroclipboard/dist/.jshintrc new file mode 100644 index 0000000..15359aa --- /dev/null +++ b/bower_components/zeroclipboard/dist/.jshintrc @@ -0,0 +1,70 @@ +{ + /* Enforcing options */ + "bitwise": true, + "camelcase": true, + "curly": true, + "eqeqeq": true, + "es3": true, + "es5": false, + "forin": true, + "freeze": true, + "immed": true, + "indent": 2, + "latedef": true, + "newcap": true, + "noarg": true, + "noempty": true, + "nonbsp": true, + "nonew": true, + "plusplus": false, + "quotmark": "double", + "undef": true, + "unused": true, + "strict": true, + "trailing": true, + "maxparams": 4, + "maxdepth": 5, + "maxstatements": false, + "maxlen": false, /* IDEAL: 120? */ + + + /* Relaxing options */ + "asi": false, + "boss": false, + "debug": false, + "eqnull": true, + "esnext": false, + "evil": false, + "expr": false, + "funcscope": false, + "gcl": false, + "globalstrict": false, + "iterator": false, + "lastsemic": false, + "laxbreak": false, + "laxcomma": false, + "loopfunc": false, + "maxerr": 50, + "moz": false, + "multistr": false, + "notypeof": false, + "proto": false, + "scripturl": false, + "smarttabs": false, + "shadow": false, + "sub": false, + "supernew": false, + "validthis": false, + "noyield": false, + + /* Environments */ + "browser": true, + + /* Global variables */ + "globals": { + /* AMD */ + "define": false, + /* CommonJS */ + "module": false + } +} \ No newline at end of file diff --git a/bower_components/zeroclipboard/dist/ZeroClipboard.Core.js b/bower_components/zeroclipboard/dist/ZeroClipboard.Core.js new file mode 100644 index 0000000..b58b146 --- /dev/null +++ b/bower_components/zeroclipboard/dist/ZeroClipboard.Core.js @@ -0,0 +1,2017 @@ +/*! + * ZeroClipboard + * The ZeroClipboard library provides an easy way to copy text to the clipboard using an invisible Adobe Flash movie and a JavaScript interface. + * Copyright (c) 2009-2014 Jon Rohan, James M. Greene + * Licensed MIT + * http://zeroclipboard.org/ + * v2.2.0 + */ +(function(window, undefined) { + "use strict"; + /** + * Store references to critically important global functions that may be + * overridden on certain web pages. + */ + var _window = window, _document = _window.document, _navigator = _window.navigator, _setTimeout = _window.setTimeout, _clearTimeout = _window.clearTimeout, _setInterval = _window.setInterval, _clearInterval = _window.clearInterval, _getComputedStyle = _window.getComputedStyle, _encodeURIComponent = _window.encodeURIComponent, _ActiveXObject = _window.ActiveXObject, _Error = _window.Error, _parseInt = _window.Number.parseInt || _window.parseInt, _parseFloat = _window.Number.parseFloat || _window.parseFloat, _isNaN = _window.Number.isNaN || _window.isNaN, _now = _window.Date.now, _keys = _window.Object.keys, _defineProperty = _window.Object.defineProperty, _hasOwn = _window.Object.prototype.hasOwnProperty, _slice = _window.Array.prototype.slice, _unwrap = function() { + var unwrapper = function(el) { + return el; + }; + if (typeof _window.wrap === "function" && typeof _window.unwrap === "function") { + try { + var div = _document.createElement("div"); + var unwrappedDiv = _window.unwrap(div); + if (div.nodeType === 1 && unwrappedDiv && unwrappedDiv.nodeType === 1) { + unwrapper = _window.unwrap; + } + } catch (e) {} + } + return unwrapper; + }(); + /** + * Convert an `arguments` object into an Array. + * + * @returns The arguments as an Array + * @private + */ + var _args = function(argumentsObj) { + return _slice.call(argumentsObj, 0); + }; + /** + * Shallow-copy the owned, enumerable properties of one object over to another, similar to jQuery's `$.extend`. + * + * @returns The target object, augmented + * @private + */ + var _extend = function() { + var i, len, arg, prop, src, copy, args = _args(arguments), target = args[0] || {}; + for (i = 1, len = args.length; i < len; i++) { + if ((arg = args[i]) != null) { + for (prop in arg) { + if (_hasOwn.call(arg, prop)) { + src = target[prop]; + copy = arg[prop]; + if (target !== copy && copy !== undefined) { + target[prop] = copy; + } + } + } + } + } + return target; + }; + /** + * Return a deep copy of the source object or array. + * + * @returns Object or Array + * @private + */ + var _deepCopy = function(source) { + var copy, i, len, prop; + if (typeof source !== "object" || source == null || typeof source.nodeType === "number") { + copy = source; + } else if (typeof source.length === "number") { + copy = []; + for (i = 0, len = source.length; i < len; i++) { + if (_hasOwn.call(source, i)) { + copy[i] = _deepCopy(source[i]); + } + } + } else { + copy = {}; + for (prop in source) { + if (_hasOwn.call(source, prop)) { + copy[prop] = _deepCopy(source[prop]); + } + } + } + return copy; + }; + /** + * Makes a shallow copy of `obj` (like `_extend`) but filters its properties based on a list of `keys` to keep. + * The inverse of `_omit`, mostly. The big difference is that these properties do NOT need to be enumerable to + * be kept. + * + * @returns A new filtered object. + * @private + */ + var _pick = function(obj, keys) { + var newObj = {}; + for (var i = 0, len = keys.length; i < len; i++) { + if (keys[i] in obj) { + newObj[keys[i]] = obj[keys[i]]; + } + } + return newObj; + }; + /** + * Makes a shallow copy of `obj` (like `_extend`) but filters its properties based on a list of `keys` to omit. + * The inverse of `_pick`. + * + * @returns A new filtered object. + * @private + */ + var _omit = function(obj, keys) { + var newObj = {}; + for (var prop in obj) { + if (keys.indexOf(prop) === -1) { + newObj[prop] = obj[prop]; + } + } + return newObj; + }; + /** + * Remove all owned, enumerable properties from an object. + * + * @returns The original object without its owned, enumerable properties. + * @private + */ + var _deleteOwnProperties = function(obj) { + if (obj) { + for (var prop in obj) { + if (_hasOwn.call(obj, prop)) { + delete obj[prop]; + } + } + } + return obj; + }; + /** + * Determine if an element is contained within another element. + * + * @returns Boolean + * @private + */ + var _containedBy = function(el, ancestorEl) { + if (el && el.nodeType === 1 && el.ownerDocument && ancestorEl && (ancestorEl.nodeType === 1 && ancestorEl.ownerDocument && ancestorEl.ownerDocument === el.ownerDocument || ancestorEl.nodeType === 9 && !ancestorEl.ownerDocument && ancestorEl === el.ownerDocument)) { + do { + if (el === ancestorEl) { + return true; + } + el = el.parentNode; + } while (el); + } + return false; + }; + /** + * Get the URL path's parent directory. + * + * @returns String or `undefined` + * @private + */ + var _getDirPathOfUrl = function(url) { + var dir; + if (typeof url === "string" && url) { + dir = url.split("#")[0].split("?")[0]; + dir = url.slice(0, url.lastIndexOf("/") + 1); + } + return dir; + }; + /** + * Get the current script's URL by throwing an `Error` and analyzing it. + * + * @returns String or `undefined` + * @private + */ + var _getCurrentScriptUrlFromErrorStack = function(stack) { + var url, matches; + if (typeof stack === "string" && stack) { + matches = stack.match(/^(?:|[^:@]*@|.+\)@(?=http[s]?|file)|.+?\s+(?: at |@)(?:[^:\(]+ )*[\(]?)((?:http[s]?|file):\/\/[\/]?.+?\/[^:\)]*?)(?::\d+)(?::\d+)?/); + if (matches && matches[1]) { + url = matches[1]; + } else { + matches = stack.match(/\)@((?:http[s]?|file):\/\/[\/]?.+?\/[^:\)]*?)(?::\d+)(?::\d+)?/); + if (matches && matches[1]) { + url = matches[1]; + } + } + } + return url; + }; + /** + * Get the current script's URL by throwing an `Error` and analyzing it. + * + * @returns String or `undefined` + * @private + */ + var _getCurrentScriptUrlFromError = function() { + var url, err; + try { + throw new _Error(); + } catch (e) { + err = e; + } + if (err) { + url = err.sourceURL || err.fileName || _getCurrentScriptUrlFromErrorStack(err.stack); + } + return url; + }; + /** + * Get the current script's URL. + * + * @returns String or `undefined` + * @private + */ + var _getCurrentScriptUrl = function() { + var jsPath, scripts, i; + if (_document.currentScript && (jsPath = _document.currentScript.src)) { + return jsPath; + } + scripts = _document.getElementsByTagName("script"); + if (scripts.length === 1) { + return scripts[0].src || undefined; + } + if ("readyState" in scripts[0]) { + for (i = scripts.length; i--; ) { + if (scripts[i].readyState === "interactive" && (jsPath = scripts[i].src)) { + return jsPath; + } + } + } + if (_document.readyState === "loading" && (jsPath = scripts[scripts.length - 1].src)) { + return jsPath; + } + if (jsPath = _getCurrentScriptUrlFromError()) { + return jsPath; + } + return undefined; + }; + /** + * Get the unanimous parent directory of ALL script tags. + * If any script tags are either (a) inline or (b) from differing parent + * directories, this method must return `undefined`. + * + * @returns String or `undefined` + * @private + */ + var _getUnanimousScriptParentDir = function() { + var i, jsDir, jsPath, scripts = _document.getElementsByTagName("script"); + for (i = scripts.length; i--; ) { + if (!(jsPath = scripts[i].src)) { + jsDir = null; + break; + } + jsPath = _getDirPathOfUrl(jsPath); + if (jsDir == null) { + jsDir = jsPath; + } else if (jsDir !== jsPath) { + jsDir = null; + break; + } + } + return jsDir || undefined; + }; + /** + * Get the presumed location of the "ZeroClipboard.swf" file, based on the location + * of the executing JavaScript file (e.g. "ZeroClipboard.js", etc.). + * + * @returns String + * @private + */ + var _getDefaultSwfPath = function() { + var jsDir = _getDirPathOfUrl(_getCurrentScriptUrl()) || _getUnanimousScriptParentDir() || ""; + return jsDir + "ZeroClipboard.swf"; + }; + /** + * Keep track of if the page is framed (in an `iframe`). This can never change. + * @private + */ + var _pageIsFramed = function() { + return window.opener == null && (!!window.top && window != window.top || !!window.parent && window != window.parent); + }(); + /** + * Keep track of the state of the Flash object. + * @private + */ + var _flashState = { + bridge: null, + version: "0.0.0", + pluginType: "unknown", + disabled: null, + outdated: null, + sandboxed: null, + unavailable: null, + degraded: null, + deactivated: null, + overdue: null, + ready: null + }; + /** + * The minimum Flash Player version required to use ZeroClipboard completely. + * @readonly + * @private + */ + var _minimumFlashVersion = "11.0.0"; + /** + * The ZeroClipboard library version number, as reported by Flash, at the time the SWF was compiled. + */ + var _zcSwfVersion; + /** + * Keep track of all event listener registrations. + * @private + */ + var _handlers = {}; + /** + * Keep track of the currently activated element. + * @private + */ + var _currentElement; + /** + * Keep track of the element that was activated when a `copy` process started. + * @private + */ + var _copyTarget; + /** + * Keep track of data for the pending clipboard transaction. + * @private + */ + var _clipData = {}; + /** + * Keep track of data formats for the pending clipboard transaction. + * @private + */ + var _clipDataFormatMap = null; + /** + * Keep track of the Flash availability check timeout. + * @private + */ + var _flashCheckTimeout = 0; + /** + * Keep track of SWF network errors interval polling. + * @private + */ + var _swfFallbackCheckInterval = 0; + /** + * The `message` store for events + * @private + */ + var _eventMessages = { + ready: "Flash communication is established", + error: { + "flash-disabled": "Flash is disabled or not installed. May also be attempting to run Flash in a sandboxed iframe, which is impossible.", + "flash-outdated": "Flash is too outdated to support ZeroClipboard", + "flash-sandboxed": "Attempting to run Flash in a sandboxed iframe, which is impossible", + "flash-unavailable": "Flash is unable to communicate bidirectionally with JavaScript", + "flash-degraded": "Flash is unable to preserve data fidelity when communicating with JavaScript", + "flash-deactivated": "Flash is too outdated for your browser and/or is configured as click-to-activate.\nThis may also mean that the ZeroClipboard SWF object could not be loaded, so please check your `swfPath` configuration and/or network connectivity.\nMay also be attempting to run Flash in a sandboxed iframe, which is impossible.", + "flash-overdue": "Flash communication was established but NOT within the acceptable time limit", + "version-mismatch": "ZeroClipboard JS version number does not match ZeroClipboard SWF version number", + "clipboard-error": "At least one error was thrown while ZeroClipboard was attempting to inject your data into the clipboard", + "config-mismatch": "ZeroClipboard configuration does not match Flash's reality", + "swf-not-found": "The ZeroClipboard SWF object could not be loaded, so please check your `swfPath` configuration and/or network connectivity" + } + }; + /** + * The `name`s of `error` events that can only occur is Flash has at least + * been able to load the SWF successfully. + * @private + */ + var _errorsThatOnlyOccurAfterFlashLoads = [ "flash-unavailable", "flash-degraded", "flash-overdue", "version-mismatch", "config-mismatch", "clipboard-error" ]; + /** + * The `name`s of `error` events that should likely result in the `_flashState` + * variable's property values being updated. + * @private + */ + var _flashStateErrorNames = [ "flash-disabled", "flash-outdated", "flash-sandboxed", "flash-unavailable", "flash-degraded", "flash-deactivated", "flash-overdue" ]; + /** + * A RegExp to match the `name` property of `error` events related to Flash. + * @private + */ + var _flashStateErrorNameMatchingRegex = new RegExp("^flash-(" + _flashStateErrorNames.map(function(errorName) { + return errorName.replace(/^flash-/, ""); + }).join("|") + ")$"); + /** + * A RegExp to match the `name` property of `error` events related to Flash, + * which is enabled. + * @private + */ + var _flashStateEnabledErrorNameMatchingRegex = new RegExp("^flash-(" + _flashStateErrorNames.slice(1).map(function(errorName) { + return errorName.replace(/^flash-/, ""); + }).join("|") + ")$"); + /** + * ZeroClipboard configuration defaults for the Core module. + * @private + */ + var _globalConfig = { + swfPath: _getDefaultSwfPath(), + trustedDomains: window.location.host ? [ window.location.host ] : [], + cacheBust: true, + forceEnhancedClipboard: false, + flashLoadTimeout: 3e4, + autoActivate: true, + bubbleEvents: true, + containerId: "global-zeroclipboard-html-bridge", + containerClass: "global-zeroclipboard-container", + swfObjectId: "global-zeroclipboard-flash-bridge", + hoverClass: "zeroclipboard-is-hover", + activeClass: "zeroclipboard-is-active", + forceHandCursor: false, + title: null, + zIndex: 999999999 + }; + /** + * The underlying implementation of `ZeroClipboard.config`. + * @private + */ + var _config = function(options) { + if (typeof options === "object" && options !== null) { + for (var prop in options) { + if (_hasOwn.call(options, prop)) { + if (/^(?:forceHandCursor|title|zIndex|bubbleEvents)$/.test(prop)) { + _globalConfig[prop] = options[prop]; + } else if (_flashState.bridge == null) { + if (prop === "containerId" || prop === "swfObjectId") { + if (_isValidHtml4Id(options[prop])) { + _globalConfig[prop] = options[prop]; + } else { + throw new Error("The specified `" + prop + "` value is not valid as an HTML4 Element ID"); + } + } else { + _globalConfig[prop] = options[prop]; + } + } + } + } + } + if (typeof options === "string" && options) { + if (_hasOwn.call(_globalConfig, options)) { + return _globalConfig[options]; + } + return; + } + return _deepCopy(_globalConfig); + }; + /** + * The underlying implementation of `ZeroClipboard.state`. + * @private + */ + var _state = function() { + _detectSandbox(); + return { + browser: _pick(_navigator, [ "userAgent", "platform", "appName" ]), + flash: _omit(_flashState, [ "bridge" ]), + zeroclipboard: { + version: ZeroClipboard.version, + config: ZeroClipboard.config() + } + }; + }; + /** + * The underlying implementation of `ZeroClipboard.isFlashUnusable`. + * @private + */ + var _isFlashUnusable = function() { + return !!(_flashState.disabled || _flashState.outdated || _flashState.sandboxed || _flashState.unavailable || _flashState.degraded || _flashState.deactivated); + }; + /** + * The underlying implementation of `ZeroClipboard.on`. + * @private + */ + var _on = function(eventType, listener) { + var i, len, events, added = {}; + if (typeof eventType === "string" && eventType) { + events = eventType.toLowerCase().split(/\s+/); + } else if (typeof eventType === "object" && eventType && typeof listener === "undefined") { + for (i in eventType) { + if (_hasOwn.call(eventType, i) && typeof i === "string" && i && typeof eventType[i] === "function") { + ZeroClipboard.on(i, eventType[i]); + } + } + } + if (events && events.length) { + for (i = 0, len = events.length; i < len; i++) { + eventType = events[i].replace(/^on/, ""); + added[eventType] = true; + if (!_handlers[eventType]) { + _handlers[eventType] = []; + } + _handlers[eventType].push(listener); + } + if (added.ready && _flashState.ready) { + ZeroClipboard.emit({ + type: "ready" + }); + } + if (added.error) { + for (i = 0, len = _flashStateErrorNames.length; i < len; i++) { + if (_flashState[_flashStateErrorNames[i].replace(/^flash-/, "")] === true) { + ZeroClipboard.emit({ + type: "error", + name: _flashStateErrorNames[i] + }); + break; + } + } + if (_zcSwfVersion !== undefined && ZeroClipboard.version !== _zcSwfVersion) { + ZeroClipboard.emit({ + type: "error", + name: "version-mismatch", + jsVersion: ZeroClipboard.version, + swfVersion: _zcSwfVersion + }); + } + } + } + return ZeroClipboard; + }; + /** + * The underlying implementation of `ZeroClipboard.off`. + * @private + */ + var _off = function(eventType, listener) { + var i, len, foundIndex, events, perEventHandlers; + if (arguments.length === 0) { + events = _keys(_handlers); + } else if (typeof eventType === "string" && eventType) { + events = eventType.split(/\s+/); + } else if (typeof eventType === "object" && eventType && typeof listener === "undefined") { + for (i in eventType) { + if (_hasOwn.call(eventType, i) && typeof i === "string" && i && typeof eventType[i] === "function") { + ZeroClipboard.off(i, eventType[i]); + } + } + } + if (events && events.length) { + for (i = 0, len = events.length; i < len; i++) { + eventType = events[i].toLowerCase().replace(/^on/, ""); + perEventHandlers = _handlers[eventType]; + if (perEventHandlers && perEventHandlers.length) { + if (listener) { + foundIndex = perEventHandlers.indexOf(listener); + while (foundIndex !== -1) { + perEventHandlers.splice(foundIndex, 1); + foundIndex = perEventHandlers.indexOf(listener, foundIndex); + } + } else { + perEventHandlers.length = 0; + } + } + } + } + return ZeroClipboard; + }; + /** + * The underlying implementation of `ZeroClipboard.handlers`. + * @private + */ + var _listeners = function(eventType) { + var copy; + if (typeof eventType === "string" && eventType) { + copy = _deepCopy(_handlers[eventType]) || null; + } else { + copy = _deepCopy(_handlers); + } + return copy; + }; + /** + * The underlying implementation of `ZeroClipboard.emit`. + * @private + */ + var _emit = function(event) { + var eventCopy, returnVal, tmp; + event = _createEvent(event); + if (!event) { + return; + } + if (_preprocessEvent(event)) { + return; + } + if (event.type === "ready" && _flashState.overdue === true) { + return ZeroClipboard.emit({ + type: "error", + name: "flash-overdue" + }); + } + eventCopy = _extend({}, event); + _dispatchCallbacks.call(this, eventCopy); + if (event.type === "copy") { + tmp = _mapClipDataToFlash(_clipData); + returnVal = tmp.data; + _clipDataFormatMap = tmp.formatMap; + } + return returnVal; + }; + /** + * The underlying implementation of `ZeroClipboard.create`. + * @private + */ + var _create = function() { + var previousState = _flashState.sandboxed; + _detectSandbox(); + if (typeof _flashState.ready !== "boolean") { + _flashState.ready = false; + } + if (_flashState.sandboxed !== previousState && _flashState.sandboxed === true) { + _flashState.ready = false; + ZeroClipboard.emit({ + type: "error", + name: "flash-sandboxed" + }); + } else if (!ZeroClipboard.isFlashUnusable() && _flashState.bridge === null) { + var maxWait = _globalConfig.flashLoadTimeout; + if (typeof maxWait === "number" && maxWait >= 0) { + _flashCheckTimeout = _setTimeout(function() { + if (typeof _flashState.deactivated !== "boolean") { + _flashState.deactivated = true; + } + if (_flashState.deactivated === true) { + ZeroClipboard.emit({ + type: "error", + name: "flash-deactivated" + }); + } + }, maxWait); + } + _flashState.overdue = false; + _embedSwf(); + } + }; + /** + * The underlying implementation of `ZeroClipboard.destroy`. + * @private + */ + var _destroy = function() { + ZeroClipboard.clearData(); + ZeroClipboard.blur(); + ZeroClipboard.emit("destroy"); + _unembedSwf(); + ZeroClipboard.off(); + }; + /** + * The underlying implementation of `ZeroClipboard.setData`. + * @private + */ + var _setData = function(format, data) { + var dataObj; + if (typeof format === "object" && format && typeof data === "undefined") { + dataObj = format; + ZeroClipboard.clearData(); + } else if (typeof format === "string" && format) { + dataObj = {}; + dataObj[format] = data; + } else { + return; + } + for (var dataFormat in dataObj) { + if (typeof dataFormat === "string" && dataFormat && _hasOwn.call(dataObj, dataFormat) && typeof dataObj[dataFormat] === "string" && dataObj[dataFormat]) { + _clipData[dataFormat] = dataObj[dataFormat]; + } + } + }; + /** + * The underlying implementation of `ZeroClipboard.clearData`. + * @private + */ + var _clearData = function(format) { + if (typeof format === "undefined") { + _deleteOwnProperties(_clipData); + _clipDataFormatMap = null; + } else if (typeof format === "string" && _hasOwn.call(_clipData, format)) { + delete _clipData[format]; + } + }; + /** + * The underlying implementation of `ZeroClipboard.getData`. + * @private + */ + var _getData = function(format) { + if (typeof format === "undefined") { + return _deepCopy(_clipData); + } else if (typeof format === "string" && _hasOwn.call(_clipData, format)) { + return _clipData[format]; + } + }; + /** + * The underlying implementation of `ZeroClipboard.focus`/`ZeroClipboard.activate`. + * @private + */ + var _focus = function(element) { + if (!(element && element.nodeType === 1)) { + return; + } + if (_currentElement) { + _removeClass(_currentElement, _globalConfig.activeClass); + if (_currentElement !== element) { + _removeClass(_currentElement, _globalConfig.hoverClass); + } + } + _currentElement = element; + _addClass(element, _globalConfig.hoverClass); + var newTitle = element.getAttribute("title") || _globalConfig.title; + if (typeof newTitle === "string" && newTitle) { + var htmlBridge = _getHtmlBridge(_flashState.bridge); + if (htmlBridge) { + htmlBridge.setAttribute("title", newTitle); + } + } + var useHandCursor = _globalConfig.forceHandCursor === true || _getStyle(element, "cursor") === "pointer"; + _setHandCursor(useHandCursor); + _reposition(); + }; + /** + * The underlying implementation of `ZeroClipboard.blur`/`ZeroClipboard.deactivate`. + * @private + */ + var _blur = function() { + var htmlBridge = _getHtmlBridge(_flashState.bridge); + if (htmlBridge) { + htmlBridge.removeAttribute("title"); + htmlBridge.style.left = "0px"; + htmlBridge.style.top = "-9999px"; + htmlBridge.style.width = "1px"; + htmlBridge.style.height = "1px"; + } + if (_currentElement) { + _removeClass(_currentElement, _globalConfig.hoverClass); + _removeClass(_currentElement, _globalConfig.activeClass); + _currentElement = null; + } + }; + /** + * The underlying implementation of `ZeroClipboard.activeElement`. + * @private + */ + var _activeElement = function() { + return _currentElement || null; + }; + /** + * Check if a value is a valid HTML4 `ID` or `Name` token. + * @private + */ + var _isValidHtml4Id = function(id) { + return typeof id === "string" && id && /^[A-Za-z][A-Za-z0-9_:\-\.]*$/.test(id); + }; + /** + * Create or update an `event` object, based on the `eventType`. + * @private + */ + var _createEvent = function(event) { + var eventType; + if (typeof event === "string" && event) { + eventType = event; + event = {}; + } else if (typeof event === "object" && event && typeof event.type === "string" && event.type) { + eventType = event.type; + } + if (!eventType) { + return; + } + eventType = eventType.toLowerCase(); + if (!event.target && (/^(copy|aftercopy|_click)$/.test(eventType) || eventType === "error" && event.name === "clipboard-error")) { + event.target = _copyTarget; + } + _extend(event, { + type: eventType, + target: event.target || _currentElement || null, + relatedTarget: event.relatedTarget || null, + currentTarget: _flashState && _flashState.bridge || null, + timeStamp: event.timeStamp || _now() || null + }); + var msg = _eventMessages[event.type]; + if (event.type === "error" && event.name && msg) { + msg = msg[event.name]; + } + if (msg) { + event.message = msg; + } + if (event.type === "ready") { + _extend(event, { + target: null, + version: _flashState.version + }); + } + if (event.type === "error") { + if (_flashStateErrorNameMatchingRegex.test(event.name)) { + _extend(event, { + target: null, + minimumVersion: _minimumFlashVersion + }); + } + if (_flashStateEnabledErrorNameMatchingRegex.test(event.name)) { + _extend(event, { + version: _flashState.version + }); + } + } + if (event.type === "copy") { + event.clipboardData = { + setData: ZeroClipboard.setData, + clearData: ZeroClipboard.clearData + }; + } + if (event.type === "aftercopy") { + event = _mapClipResultsFromFlash(event, _clipDataFormatMap); + } + if (event.target && !event.relatedTarget) { + event.relatedTarget = _getRelatedTarget(event.target); + } + return _addMouseData(event); + }; + /** + * Get a relatedTarget from the target's `data-clipboard-target` attribute + * @private + */ + var _getRelatedTarget = function(targetEl) { + var relatedTargetId = targetEl && targetEl.getAttribute && targetEl.getAttribute("data-clipboard-target"); + return relatedTargetId ? _document.getElementById(relatedTargetId) : null; + }; + /** + * Add element and position data to `MouseEvent` instances + * @private + */ + var _addMouseData = function(event) { + if (event && /^_(?:click|mouse(?:over|out|down|up|move))$/.test(event.type)) { + var srcElement = event.target; + var fromElement = event.type === "_mouseover" && event.relatedTarget ? event.relatedTarget : undefined; + var toElement = event.type === "_mouseout" && event.relatedTarget ? event.relatedTarget : undefined; + var pos = _getElementPosition(srcElement); + var screenLeft = _window.screenLeft || _window.screenX || 0; + var screenTop = _window.screenTop || _window.screenY || 0; + var scrollLeft = _document.body.scrollLeft + _document.documentElement.scrollLeft; + var scrollTop = _document.body.scrollTop + _document.documentElement.scrollTop; + var pageX = pos.left + (typeof event._stageX === "number" ? event._stageX : 0); + var pageY = pos.top + (typeof event._stageY === "number" ? event._stageY : 0); + var clientX = pageX - scrollLeft; + var clientY = pageY - scrollTop; + var screenX = screenLeft + clientX; + var screenY = screenTop + clientY; + var moveX = typeof event.movementX === "number" ? event.movementX : 0; + var moveY = typeof event.movementY === "number" ? event.movementY : 0; + delete event._stageX; + delete event._stageY; + _extend(event, { + srcElement: srcElement, + fromElement: fromElement, + toElement: toElement, + screenX: screenX, + screenY: screenY, + pageX: pageX, + pageY: pageY, + clientX: clientX, + clientY: clientY, + x: clientX, + y: clientY, + movementX: moveX, + movementY: moveY, + offsetX: 0, + offsetY: 0, + layerX: 0, + layerY: 0 + }); + } + return event; + }; + /** + * Determine if an event's registered handlers should be execute synchronously or asynchronously. + * + * @returns {boolean} + * @private + */ + var _shouldPerformAsync = function(event) { + var eventType = event && typeof event.type === "string" && event.type || ""; + return !/^(?:(?:before)?copy|destroy)$/.test(eventType); + }; + /** + * Control if a callback should be executed asynchronously or not. + * + * @returns `undefined` + * @private + */ + var _dispatchCallback = function(func, context, args, async) { + if (async) { + _setTimeout(function() { + func.apply(context, args); + }, 0); + } else { + func.apply(context, args); + } + }; + /** + * Handle the actual dispatching of events to client instances. + * + * @returns `undefined` + * @private + */ + var _dispatchCallbacks = function(event) { + if (!(typeof event === "object" && event && event.type)) { + return; + } + var async = _shouldPerformAsync(event); + var wildcardTypeHandlers = _handlers["*"] || []; + var specificTypeHandlers = _handlers[event.type] || []; + var handlers = wildcardTypeHandlers.concat(specificTypeHandlers); + if (handlers && handlers.length) { + var i, len, func, context, eventCopy, originalContext = this; + for (i = 0, len = handlers.length; i < len; i++) { + func = handlers[i]; + context = originalContext; + if (typeof func === "string" && typeof _window[func] === "function") { + func = _window[func]; + } + if (typeof func === "object" && func && typeof func.handleEvent === "function") { + context = func; + func = func.handleEvent; + } + if (typeof func === "function") { + eventCopy = _extend({}, event); + _dispatchCallback(func, context, [ eventCopy ], async); + } + } + } + return this; + }; + /** + * Check an `error` event's `name` property to see if Flash has + * already loaded, which rules out possible `iframe` sandboxing. + * @private + */ + var _getSandboxStatusFromErrorEvent = function(event) { + var isSandboxed = null; + if (_pageIsFramed === false || event && event.type === "error" && event.name && _errorsThatOnlyOccurAfterFlashLoads.indexOf(event.name) !== -1) { + isSandboxed = false; + } + return isSandboxed; + }; + /** + * Preprocess any special behaviors, reactions, or state changes after receiving this event. + * Executes only once per event emitted, NOT once per client. + * @private + */ + var _preprocessEvent = function(event) { + var element = event.target || _currentElement || null; + var sourceIsSwf = event._source === "swf"; + delete event._source; + switch (event.type) { + case "error": + var isSandboxed = event.name === "flash-sandboxed" || _getSandboxStatusFromErrorEvent(event); + if (typeof isSandboxed === "boolean") { + _flashState.sandboxed = isSandboxed; + } + if (_flashStateErrorNames.indexOf(event.name) !== -1) { + _extend(_flashState, { + disabled: event.name === "flash-disabled", + outdated: event.name === "flash-outdated", + unavailable: event.name === "flash-unavailable", + degraded: event.name === "flash-degraded", + deactivated: event.name === "flash-deactivated", + overdue: event.name === "flash-overdue", + ready: false + }); + } else if (event.name === "version-mismatch") { + _zcSwfVersion = event.swfVersion; + _extend(_flashState, { + disabled: false, + outdated: false, + unavailable: false, + degraded: false, + deactivated: false, + overdue: false, + ready: false + }); + } + _clearTimeoutsAndPolling(); + break; + + case "ready": + _zcSwfVersion = event.swfVersion; + var wasDeactivated = _flashState.deactivated === true; + _extend(_flashState, { + disabled: false, + outdated: false, + sandboxed: false, + unavailable: false, + degraded: false, + deactivated: false, + overdue: wasDeactivated, + ready: !wasDeactivated + }); + _clearTimeoutsAndPolling(); + break; + + case "beforecopy": + _copyTarget = element; + break; + + case "copy": + var textContent, htmlContent, targetEl = event.relatedTarget; + if (!(_clipData["text/html"] || _clipData["text/plain"]) && targetEl && (htmlContent = targetEl.value || targetEl.outerHTML || targetEl.innerHTML) && (textContent = targetEl.value || targetEl.textContent || targetEl.innerText)) { + event.clipboardData.clearData(); + event.clipboardData.setData("text/plain", textContent); + if (htmlContent !== textContent) { + event.clipboardData.setData("text/html", htmlContent); + } + } else if (!_clipData["text/plain"] && event.target && (textContent = event.target.getAttribute("data-clipboard-text"))) { + event.clipboardData.clearData(); + event.clipboardData.setData("text/plain", textContent); + } + break; + + case "aftercopy": + _queueEmitClipboardErrors(event); + ZeroClipboard.clearData(); + if (element && element !== _safeActiveElement() && element.focus) { + element.focus(); + } + break; + + case "_mouseover": + ZeroClipboard.focus(element); + if (_globalConfig.bubbleEvents === true && sourceIsSwf) { + if (element && element !== event.relatedTarget && !_containedBy(event.relatedTarget, element)) { + _fireMouseEvent(_extend({}, event, { + type: "mouseenter", + bubbles: false, + cancelable: false + })); + } + _fireMouseEvent(_extend({}, event, { + type: "mouseover" + })); + } + break; + + case "_mouseout": + ZeroClipboard.blur(); + if (_globalConfig.bubbleEvents === true && sourceIsSwf) { + if (element && element !== event.relatedTarget && !_containedBy(event.relatedTarget, element)) { + _fireMouseEvent(_extend({}, event, { + type: "mouseleave", + bubbles: false, + cancelable: false + })); + } + _fireMouseEvent(_extend({}, event, { + type: "mouseout" + })); + } + break; + + case "_mousedown": + _addClass(element, _globalConfig.activeClass); + if (_globalConfig.bubbleEvents === true && sourceIsSwf) { + _fireMouseEvent(_extend({}, event, { + type: event.type.slice(1) + })); + } + break; + + case "_mouseup": + _removeClass(element, _globalConfig.activeClass); + if (_globalConfig.bubbleEvents === true && sourceIsSwf) { + _fireMouseEvent(_extend({}, event, { + type: event.type.slice(1) + })); + } + break; + + case "_click": + _copyTarget = null; + if (_globalConfig.bubbleEvents === true && sourceIsSwf) { + _fireMouseEvent(_extend({}, event, { + type: event.type.slice(1) + })); + } + break; + + case "_mousemove": + if (_globalConfig.bubbleEvents === true && sourceIsSwf) { + _fireMouseEvent(_extend({}, event, { + type: event.type.slice(1) + })); + } + break; + } + if (/^_(?:click|mouse(?:over|out|down|up|move))$/.test(event.type)) { + return true; + } + }; + /** + * Check an "aftercopy" event for clipboard errors and emit a corresponding "error" event. + * @private + */ + var _queueEmitClipboardErrors = function(aftercopyEvent) { + if (aftercopyEvent.errors && aftercopyEvent.errors.length > 0) { + var errorEvent = _deepCopy(aftercopyEvent); + _extend(errorEvent, { + type: "error", + name: "clipboard-error" + }); + delete errorEvent.success; + _setTimeout(function() { + ZeroClipboard.emit(errorEvent); + }, 0); + } + }; + /** + * Dispatch a synthetic MouseEvent. + * + * @returns `undefined` + * @private + */ + var _fireMouseEvent = function(event) { + if (!(event && typeof event.type === "string" && event)) { + return; + } + var e, target = event.target || null, doc = target && target.ownerDocument || _document, defaults = { + view: doc.defaultView || _window, + canBubble: true, + cancelable: true, + detail: event.type === "click" ? 1 : 0, + button: typeof event.which === "number" ? event.which - 1 : typeof event.button === "number" ? event.button : doc.createEvent ? 0 : 1 + }, args = _extend(defaults, event); + if (!target) { + return; + } + if (doc.createEvent && target.dispatchEvent) { + args = [ args.type, args.canBubble, args.cancelable, args.view, args.detail, args.screenX, args.screenY, args.clientX, args.clientY, args.ctrlKey, args.altKey, args.shiftKey, args.metaKey, args.button, args.relatedTarget ]; + e = doc.createEvent("MouseEvents"); + if (e.initMouseEvent) { + e.initMouseEvent.apply(e, args); + e._source = "js"; + target.dispatchEvent(e); + } + } + }; + /** + * Continuously poll the DOM until either: + * (a) the fallback content becomes visible, or + * (b) we receive an event from SWF (handled elsewhere) + * + * IMPORTANT: + * This is NOT a necessary check but it can result in significantly faster + * detection of bad `swfPath` configuration and/or network/server issues [in + * supported browsers] than waiting for the entire `flashLoadTimeout` duration + * to elapse before detecting that the SWF cannot be loaded. The detection + * duration can be anywhere from 10-30 times faster [in supported browsers] by + * using this approach. + * + * @returns `undefined` + * @private + */ + var _watchForSwfFallbackContent = function() { + var maxWait = _globalConfig.flashLoadTimeout; + if (typeof maxWait === "number" && maxWait >= 0) { + var pollWait = Math.min(1e3, maxWait / 10); + var fallbackContentId = _globalConfig.swfObjectId + "_fallbackContent"; + _swfFallbackCheckInterval = _setInterval(function() { + var el = _document.getElementById(fallbackContentId); + if (_isElementVisible(el)) { + _clearTimeoutsAndPolling(); + _flashState.deactivated = null; + ZeroClipboard.emit({ + type: "error", + name: "swf-not-found" + }); + } + }, pollWait); + } + }; + /** + * Create the HTML bridge element to embed the Flash object into. + * @private + */ + var _createHtmlBridge = function() { + var container = _document.createElement("div"); + container.id = _globalConfig.containerId; + container.className = _globalConfig.containerClass; + container.style.position = "absolute"; + container.style.left = "0px"; + container.style.top = "-9999px"; + container.style.width = "1px"; + container.style.height = "1px"; + container.style.zIndex = "" + _getSafeZIndex(_globalConfig.zIndex); + return container; + }; + /** + * Get the HTML element container that wraps the Flash bridge object/element. + * @private + */ + var _getHtmlBridge = function(flashBridge) { + var htmlBridge = flashBridge && flashBridge.parentNode; + while (htmlBridge && htmlBridge.nodeName === "OBJECT" && htmlBridge.parentNode) { + htmlBridge = htmlBridge.parentNode; + } + return htmlBridge || null; + }; + /** + * Create the SWF object. + * + * @returns The SWF object reference. + * @private + */ + var _embedSwf = function() { + var len, flashBridge = _flashState.bridge, container = _getHtmlBridge(flashBridge); + if (!flashBridge) { + var allowScriptAccess = _determineScriptAccess(_window.location.host, _globalConfig); + var allowNetworking = allowScriptAccess === "never" ? "none" : "all"; + var flashvars = _vars(_extend({ + jsVersion: ZeroClipboard.version + }, _globalConfig)); + var swfUrl = _globalConfig.swfPath + _cacheBust(_globalConfig.swfPath, _globalConfig); + container = _createHtmlBridge(); + var divToBeReplaced = _document.createElement("div"); + container.appendChild(divToBeReplaced); + _document.body.appendChild(container); + var tmpDiv = _document.createElement("div"); + var usingActiveX = _flashState.pluginType === "activex"; + tmpDiv.innerHTML = '" + (usingActiveX ? '' : "") + '' + '' + '' + '' + '' + '
 
' + "
"; + flashBridge = tmpDiv.firstChild; + tmpDiv = null; + _unwrap(flashBridge).ZeroClipboard = ZeroClipboard; + container.replaceChild(flashBridge, divToBeReplaced); + _watchForSwfFallbackContent(); + } + if (!flashBridge) { + flashBridge = _document[_globalConfig.swfObjectId]; + if (flashBridge && (len = flashBridge.length)) { + flashBridge = flashBridge[len - 1]; + } + if (!flashBridge && container) { + flashBridge = container.firstChild; + } + } + _flashState.bridge = flashBridge || null; + return flashBridge; + }; + /** + * Destroy the SWF object. + * @private + */ + var _unembedSwf = function() { + var flashBridge = _flashState.bridge; + if (flashBridge) { + var htmlBridge = _getHtmlBridge(flashBridge); + if (htmlBridge) { + if (_flashState.pluginType === "activex" && "readyState" in flashBridge) { + flashBridge.style.display = "none"; + (function removeSwfFromIE() { + if (flashBridge.readyState === 4) { + for (var prop in flashBridge) { + if (typeof flashBridge[prop] === "function") { + flashBridge[prop] = null; + } + } + if (flashBridge.parentNode) { + flashBridge.parentNode.removeChild(flashBridge); + } + if (htmlBridge.parentNode) { + htmlBridge.parentNode.removeChild(htmlBridge); + } + } else { + _setTimeout(removeSwfFromIE, 10); + } + })(); + } else { + if (flashBridge.parentNode) { + flashBridge.parentNode.removeChild(flashBridge); + } + if (htmlBridge.parentNode) { + htmlBridge.parentNode.removeChild(htmlBridge); + } + } + } + _clearTimeoutsAndPolling(); + _flashState.ready = null; + _flashState.bridge = null; + _flashState.deactivated = null; + _zcSwfVersion = undefined; + } + }; + /** + * Map the data format names of the "clipData" to Flash-friendly names. + * + * @returns A new transformed object. + * @private + */ + var _mapClipDataToFlash = function(clipData) { + var newClipData = {}, formatMap = {}; + if (!(typeof clipData === "object" && clipData)) { + return; + } + for (var dataFormat in clipData) { + if (dataFormat && _hasOwn.call(clipData, dataFormat) && typeof clipData[dataFormat] === "string" && clipData[dataFormat]) { + switch (dataFormat.toLowerCase()) { + case "text/plain": + case "text": + case "air:text": + case "flash:text": + newClipData.text = clipData[dataFormat]; + formatMap.text = dataFormat; + break; + + case "text/html": + case "html": + case "air:html": + case "flash:html": + newClipData.html = clipData[dataFormat]; + formatMap.html = dataFormat; + break; + + case "application/rtf": + case "text/rtf": + case "rtf": + case "richtext": + case "air:rtf": + case "flash:rtf": + newClipData.rtf = clipData[dataFormat]; + formatMap.rtf = dataFormat; + break; + + default: + break; + } + } + } + return { + data: newClipData, + formatMap: formatMap + }; + }; + /** + * Map the data format names from Flash-friendly names back to their original "clipData" names (via a format mapping). + * + * @returns A new transformed object. + * @private + */ + var _mapClipResultsFromFlash = function(clipResults, formatMap) { + if (!(typeof clipResults === "object" && clipResults && typeof formatMap === "object" && formatMap)) { + return clipResults; + } + var newResults = {}; + for (var prop in clipResults) { + if (_hasOwn.call(clipResults, prop)) { + if (prop === "errors") { + newResults[prop] = clipResults[prop] ? clipResults[prop].slice() : []; + for (var i = 0, len = newResults[prop].length; i < len; i++) { + newResults[prop][i].format = formatMap[newResults[prop][i].format]; + } + } else if (prop !== "success" && prop !== "data") { + newResults[prop] = clipResults[prop]; + } else { + newResults[prop] = {}; + var tmpHash = clipResults[prop]; + for (var dataFormat in tmpHash) { + if (dataFormat && _hasOwn.call(tmpHash, dataFormat) && _hasOwn.call(formatMap, dataFormat)) { + newResults[prop][formatMap[dataFormat]] = tmpHash[dataFormat]; + } + } + } + } + } + return newResults; + }; + /** + * Will look at a path, and will create a "?noCache={time}" or "&noCache={time}" + * query param string to return. Does NOT append that string to the original path. + * This is useful because ExternalInterface often breaks when a Flash SWF is cached. + * + * @returns The `noCache` query param with necessary "?"/"&" prefix. + * @private + */ + var _cacheBust = function(path, options) { + var cacheBust = options == null || options && options.cacheBust === true; + if (cacheBust) { + return (path.indexOf("?") === -1 ? "?" : "&") + "noCache=" + _now(); + } else { + return ""; + } + }; + /** + * Creates a query string for the FlashVars param. + * Does NOT include the cache-busting query param. + * + * @returns FlashVars query string + * @private + */ + var _vars = function(options) { + var i, len, domain, domains, str = "", trustedOriginsExpanded = []; + if (options.trustedDomains) { + if (typeof options.trustedDomains === "string") { + domains = [ options.trustedDomains ]; + } else if (typeof options.trustedDomains === "object" && "length" in options.trustedDomains) { + domains = options.trustedDomains; + } + } + if (domains && domains.length) { + for (i = 0, len = domains.length; i < len; i++) { + if (_hasOwn.call(domains, i) && domains[i] && typeof domains[i] === "string") { + domain = _extractDomain(domains[i]); + if (!domain) { + continue; + } + if (domain === "*") { + trustedOriginsExpanded.length = 0; + trustedOriginsExpanded.push(domain); + break; + } + trustedOriginsExpanded.push.apply(trustedOriginsExpanded, [ domain, "//" + domain, _window.location.protocol + "//" + domain ]); + } + } + } + if (trustedOriginsExpanded.length) { + str += "trustedOrigins=" + _encodeURIComponent(trustedOriginsExpanded.join(",")); + } + if (options.forceEnhancedClipboard === true) { + str += (str ? "&" : "") + "forceEnhancedClipboard=true"; + } + if (typeof options.swfObjectId === "string" && options.swfObjectId) { + str += (str ? "&" : "") + "swfObjectId=" + _encodeURIComponent(options.swfObjectId); + } + if (typeof options.jsVersion === "string" && options.jsVersion) { + str += (str ? "&" : "") + "jsVersion=" + _encodeURIComponent(options.jsVersion); + } + return str; + }; + /** + * Extract the domain (e.g. "github.com") from an origin (e.g. "https://github.com") or + * URL (e.g. "https://github.com/zeroclipboard/zeroclipboard/"). + * + * @returns the domain + * @private + */ + var _extractDomain = function(originOrUrl) { + if (originOrUrl == null || originOrUrl === "") { + return null; + } + originOrUrl = originOrUrl.replace(/^\s+|\s+$/g, ""); + if (originOrUrl === "") { + return null; + } + var protocolIndex = originOrUrl.indexOf("//"); + originOrUrl = protocolIndex === -1 ? originOrUrl : originOrUrl.slice(protocolIndex + 2); + var pathIndex = originOrUrl.indexOf("/"); + originOrUrl = pathIndex === -1 ? originOrUrl : protocolIndex === -1 || pathIndex === 0 ? null : originOrUrl.slice(0, pathIndex); + if (originOrUrl && originOrUrl.slice(-4).toLowerCase() === ".swf") { + return null; + } + return originOrUrl || null; + }; + /** + * Set `allowScriptAccess` based on `trustedDomains` and `window.location.host` vs. `swfPath`. + * + * @returns The appropriate script access level. + * @private + */ + var _determineScriptAccess = function() { + var _extractAllDomains = function(origins) { + var i, len, tmp, resultsArray = []; + if (typeof origins === "string") { + origins = [ origins ]; + } + if (!(typeof origins === "object" && origins && typeof origins.length === "number")) { + return resultsArray; + } + for (i = 0, len = origins.length; i < len; i++) { + if (_hasOwn.call(origins, i) && (tmp = _extractDomain(origins[i]))) { + if (tmp === "*") { + resultsArray.length = 0; + resultsArray.push("*"); + break; + } + if (resultsArray.indexOf(tmp) === -1) { + resultsArray.push(tmp); + } + } + } + return resultsArray; + }; + return function(currentDomain, configOptions) { + var swfDomain = _extractDomain(configOptions.swfPath); + if (swfDomain === null) { + swfDomain = currentDomain; + } + var trustedDomains = _extractAllDomains(configOptions.trustedDomains); + var len = trustedDomains.length; + if (len > 0) { + if (len === 1 && trustedDomains[0] === "*") { + return "always"; + } + if (trustedDomains.indexOf(currentDomain) !== -1) { + if (len === 1 && currentDomain === swfDomain) { + return "sameDomain"; + } + return "always"; + } + } + return "never"; + }; + }(); + /** + * Get the currently active/focused DOM element. + * + * @returns the currently active/focused element, or `null` + * @private + */ + var _safeActiveElement = function() { + try { + return _document.activeElement; + } catch (err) { + return null; + } + }; + /** + * Add a class to an element, if it doesn't already have it. + * + * @returns The element, with its new class added. + * @private + */ + var _addClass = function(element, value) { + var c, cl, className, classNames = []; + if (typeof value === "string" && value) { + classNames = value.split(/\s+/); + } + if (element && element.nodeType === 1 && classNames.length > 0) { + if (element.classList) { + for (c = 0, cl = classNames.length; c < cl; c++) { + element.classList.add(classNames[c]); + } + } else if (element.hasOwnProperty("className")) { + className = " " + element.className + " "; + for (c = 0, cl = classNames.length; c < cl; c++) { + if (className.indexOf(" " + classNames[c] + " ") === -1) { + className += classNames[c] + " "; + } + } + element.className = className.replace(/^\s+|\s+$/g, ""); + } + } + return element; + }; + /** + * Remove a class from an element, if it has it. + * + * @returns The element, with its class removed. + * @private + */ + var _removeClass = function(element, value) { + var c, cl, className, classNames = []; + if (typeof value === "string" && value) { + classNames = value.split(/\s+/); + } + if (element && element.nodeType === 1 && classNames.length > 0) { + if (element.classList && element.classList.length > 0) { + for (c = 0, cl = classNames.length; c < cl; c++) { + element.classList.remove(classNames[c]); + } + } else if (element.className) { + className = (" " + element.className + " ").replace(/[\r\n\t]/g, " "); + for (c = 0, cl = classNames.length; c < cl; c++) { + className = className.replace(" " + classNames[c] + " ", " "); + } + element.className = className.replace(/^\s+|\s+$/g, ""); + } + } + return element; + }; + /** + * Attempt to interpret the element's CSS styling. If `prop` is `"cursor"`, + * then we assume that it should be a hand ("pointer") cursor if the element + * is an anchor element ("a" tag). + * + * @returns The computed style property. + * @private + */ + var _getStyle = function(el, prop) { + var value = _getComputedStyle(el, null).getPropertyValue(prop); + if (prop === "cursor") { + if (!value || value === "auto") { + if (el.nodeName === "A") { + return "pointer"; + } + } + } + return value; + }; + /** + * Get the absolutely positioned coordinates of a DOM element. + * + * @returns Object containing the element's position, width, and height. + * @private + */ + var _getElementPosition = function(el) { + var pos = { + left: 0, + top: 0, + width: 0, + height: 0 + }; + if (el.getBoundingClientRect) { + var elRect = el.getBoundingClientRect(); + var pageXOffset = _window.pageXOffset; + var pageYOffset = _window.pageYOffset; + var leftBorderWidth = _document.documentElement.clientLeft || 0; + var topBorderWidth = _document.documentElement.clientTop || 0; + var leftBodyOffset = 0; + var topBodyOffset = 0; + if (_getStyle(_document.body, "position") === "relative") { + var bodyRect = _document.body.getBoundingClientRect(); + var htmlRect = _document.documentElement.getBoundingClientRect(); + leftBodyOffset = bodyRect.left - htmlRect.left || 0; + topBodyOffset = bodyRect.top - htmlRect.top || 0; + } + pos.left = elRect.left + pageXOffset - leftBorderWidth - leftBodyOffset; + pos.top = elRect.top + pageYOffset - topBorderWidth - topBodyOffset; + pos.width = "width" in elRect ? elRect.width : elRect.right - elRect.left; + pos.height = "height" in elRect ? elRect.height : elRect.bottom - elRect.top; + } + return pos; + }; + /** + * Determine is an element is visible somewhere within the document (page). + * + * @returns Boolean + * @private + */ + var _isElementVisible = function(el) { + if (!el) { + return false; + } + var styles = _getComputedStyle(el, null); + var hasCssHeight = _parseFloat(styles.height) > 0; + var hasCssWidth = _parseFloat(styles.width) > 0; + var hasCssTop = _parseFloat(styles.top) >= 0; + var hasCssLeft = _parseFloat(styles.left) >= 0; + var cssKnows = hasCssHeight && hasCssWidth && hasCssTop && hasCssLeft; + var rect = cssKnows ? null : _getElementPosition(el); + var isVisible = styles.display !== "none" && styles.visibility !== "collapse" && (cssKnows || !!rect && (hasCssHeight || rect.height > 0) && (hasCssWidth || rect.width > 0) && (hasCssTop || rect.top >= 0) && (hasCssLeft || rect.left >= 0)); + return isVisible; + }; + /** + * Clear all existing timeouts and interval polling delegates. + * + * @returns `undefined` + * @private + */ + var _clearTimeoutsAndPolling = function() { + _clearTimeout(_flashCheckTimeout); + _flashCheckTimeout = 0; + _clearInterval(_swfFallbackCheckInterval); + _swfFallbackCheckInterval = 0; + }; + /** + * Reposition the Flash object to cover the currently activated element. + * + * @returns `undefined` + * @private + */ + var _reposition = function() { + var htmlBridge; + if (_currentElement && (htmlBridge = _getHtmlBridge(_flashState.bridge))) { + var pos = _getElementPosition(_currentElement); + _extend(htmlBridge.style, { + width: pos.width + "px", + height: pos.height + "px", + top: pos.top + "px", + left: pos.left + "px", + zIndex: "" + _getSafeZIndex(_globalConfig.zIndex) + }); + } + }; + /** + * Sends a signal to the Flash object to display the hand cursor if `true`. + * + * @returns `undefined` + * @private + */ + var _setHandCursor = function(enabled) { + if (_flashState.ready === true) { + if (_flashState.bridge && typeof _flashState.bridge.setHandCursor === "function") { + _flashState.bridge.setHandCursor(enabled); + } else { + _flashState.ready = false; + } + } + }; + /** + * Get a safe value for `zIndex` + * + * @returns an integer, or "auto" + * @private + */ + var _getSafeZIndex = function(val) { + if (/^(?:auto|inherit)$/.test(val)) { + return val; + } + var zIndex; + if (typeof val === "number" && !_isNaN(val)) { + zIndex = val; + } else if (typeof val === "string") { + zIndex = _getSafeZIndex(_parseInt(val, 10)); + } + return typeof zIndex === "number" ? zIndex : "auto"; + }; + /** + * Attempt to detect if ZeroClipboard is executing inside of a sandboxed iframe. + * If it is, Flash Player cannot be used, so ZeroClipboard is dead in the water. + * + * @see {@link http://lists.w3.org/Archives/Public/public-whatwg-archive/2014Dec/0002.html} + * @see {@link https://github.com/zeroclipboard/zeroclipboard/issues/511} + * @see {@link http://zeroclipboard.org/test-iframes.html} + * + * @returns `true` (is sandboxed), `false` (is not sandboxed), or `null` (uncertain) + * @private + */ + var _detectSandbox = function(doNotReassessFlashSupport) { + var effectiveScriptOrigin, frame, frameError, previousState = _flashState.sandboxed, isSandboxed = null; + doNotReassessFlashSupport = doNotReassessFlashSupport === true; + if (_pageIsFramed === false) { + isSandboxed = false; + } else { + try { + frame = window.frameElement || null; + } catch (e) { + frameError = { + name: e.name, + message: e.message + }; + } + if (frame && frame.nodeType === 1 && frame.nodeName === "IFRAME") { + try { + isSandboxed = frame.hasAttribute("sandbox"); + } catch (e) { + isSandboxed = null; + } + } else { + try { + effectiveScriptOrigin = document.domain || null; + } catch (e) { + effectiveScriptOrigin = null; + } + if (effectiveScriptOrigin === null || frameError && frameError.name === "SecurityError" && /(^|[\s\(\[@])sandbox(es|ed|ing|[\s\.,!\)\]@]|$)/.test(frameError.message.toLowerCase())) { + isSandboxed = true; + } + } + } + _flashState.sandboxed = isSandboxed; + if (previousState !== isSandboxed && !doNotReassessFlashSupport) { + _detectFlashSupport(_ActiveXObject); + } + return isSandboxed; + }; + /** + * Detect the Flash Player status, version, and plugin type. + * + * @see {@link https://code.google.com/p/doctype-mirror/wiki/ArticleDetectFlash#The_code} + * @see {@link http://stackoverflow.com/questions/12866060/detecting-pepper-ppapi-flash-with-javascript} + * + * @returns `undefined` + * @private + */ + var _detectFlashSupport = function(ActiveXObject) { + var plugin, ax, mimeType, hasFlash = false, isActiveX = false, isPPAPI = false, flashVersion = ""; + /** + * Derived from Apple's suggested sniffer. + * @param {String} desc e.g. "Shockwave Flash 7.0 r61" + * @returns {String} "7.0.61" + * @private + */ + function parseFlashVersion(desc) { + var matches = desc.match(/[\d]+/g); + matches.length = 3; + return matches.join("."); + } + function isPepperFlash(flashPlayerFileName) { + return !!flashPlayerFileName && (flashPlayerFileName = flashPlayerFileName.toLowerCase()) && (/^(pepflashplayer\.dll|libpepflashplayer\.so|pepperflashplayer\.plugin)$/.test(flashPlayerFileName) || flashPlayerFileName.slice(-13) === "chrome.plugin"); + } + function inspectPlugin(plugin) { + if (plugin) { + hasFlash = true; + if (plugin.version) { + flashVersion = parseFlashVersion(plugin.version); + } + if (!flashVersion && plugin.description) { + flashVersion = parseFlashVersion(plugin.description); + } + if (plugin.filename) { + isPPAPI = isPepperFlash(plugin.filename); + } + } + } + if (_navigator.plugins && _navigator.plugins.length) { + plugin = _navigator.plugins["Shockwave Flash"]; + inspectPlugin(plugin); + if (_navigator.plugins["Shockwave Flash 2.0"]) { + hasFlash = true; + flashVersion = "2.0.0.11"; + } + } else if (_navigator.mimeTypes && _navigator.mimeTypes.length) { + mimeType = _navigator.mimeTypes["application/x-shockwave-flash"]; + plugin = mimeType && mimeType.enabledPlugin; + inspectPlugin(plugin); + } else if (typeof ActiveXObject !== "undefined") { + isActiveX = true; + try { + ax = new ActiveXObject("ShockwaveFlash.ShockwaveFlash.7"); + hasFlash = true; + flashVersion = parseFlashVersion(ax.GetVariable("$version")); + } catch (e1) { + try { + ax = new ActiveXObject("ShockwaveFlash.ShockwaveFlash.6"); + hasFlash = true; + flashVersion = "6.0.21"; + } catch (e2) { + try { + ax = new ActiveXObject("ShockwaveFlash.ShockwaveFlash"); + hasFlash = true; + flashVersion = parseFlashVersion(ax.GetVariable("$version")); + } catch (e3) { + isActiveX = false; + } + } + } + } + _flashState.disabled = hasFlash !== true; + _flashState.outdated = flashVersion && _parseFloat(flashVersion) < _parseFloat(_minimumFlashVersion); + _flashState.version = flashVersion || "0.0.0"; + _flashState.pluginType = isPPAPI ? "pepper" : isActiveX ? "activex" : hasFlash ? "netscape" : "unknown"; + }; + /** + * Invoke the Flash detection algorithms immediately upon inclusion so we're not waiting later. + */ + _detectFlashSupport(_ActiveXObject); + /** + * Always assess the `sandboxed` state of the page at important Flash-related moments. + */ + _detectSandbox(true); + /** + * A shell constructor for `ZeroClipboard` client instances. + * + * @constructor + */ + var ZeroClipboard = function() { + if (!(this instanceof ZeroClipboard)) { + return new ZeroClipboard(); + } + if (typeof ZeroClipboard._createClient === "function") { + ZeroClipboard._createClient.apply(this, _args(arguments)); + } + }; + /** + * The ZeroClipboard library's version number. + * + * @static + * @readonly + * @property {string} + */ + _defineProperty(ZeroClipboard, "version", { + value: "2.2.0", + writable: false, + configurable: true, + enumerable: true + }); + /** + * Update or get a copy of the ZeroClipboard global configuration. + * Returns a copy of the current/updated configuration. + * + * @returns Object + * @static + */ + ZeroClipboard.config = function() { + return _config.apply(this, _args(arguments)); + }; + /** + * Diagnostic method that describes the state of the browser, Flash Player, and ZeroClipboard. + * + * @returns Object + * @static + */ + ZeroClipboard.state = function() { + return _state.apply(this, _args(arguments)); + }; + /** + * Check if Flash is unusable for any reason: disabled, outdated, deactivated, etc. + * + * @returns Boolean + * @static + */ + ZeroClipboard.isFlashUnusable = function() { + return _isFlashUnusable.apply(this, _args(arguments)); + }; + /** + * Register an event listener. + * + * @returns `ZeroClipboard` + * @static + */ + ZeroClipboard.on = function() { + return _on.apply(this, _args(arguments)); + }; + /** + * Unregister an event listener. + * If no `listener` function/object is provided, it will unregister all listeners for the provided `eventType`. + * If no `eventType` is provided, it will unregister all listeners for every event type. + * + * @returns `ZeroClipboard` + * @static + */ + ZeroClipboard.off = function() { + return _off.apply(this, _args(arguments)); + }; + /** + * Retrieve event listeners for an `eventType`. + * If no `eventType` is provided, it will retrieve all listeners for every event type. + * + * @returns array of listeners for the `eventType`; if no `eventType`, then a map/hash object of listeners for all event types; or `null` + */ + ZeroClipboard.handlers = function() { + return _listeners.apply(this, _args(arguments)); + }; + /** + * Event emission receiver from the Flash object, forwarding to any registered JavaScript event listeners. + * + * @returns For the "copy" event, returns the Flash-friendly "clipData" object; otherwise `undefined`. + * @static + */ + ZeroClipboard.emit = function() { + return _emit.apply(this, _args(arguments)); + }; + /** + * Create and embed the Flash object. + * + * @returns The Flash object + * @static + */ + ZeroClipboard.create = function() { + return _create.apply(this, _args(arguments)); + }; + /** + * Self-destruct and clean up everything, including the embedded Flash object. + * + * @returns `undefined` + * @static + */ + ZeroClipboard.destroy = function() { + return _destroy.apply(this, _args(arguments)); + }; + /** + * Set the pending data for clipboard injection. + * + * @returns `undefined` + * @static + */ + ZeroClipboard.setData = function() { + return _setData.apply(this, _args(arguments)); + }; + /** + * Clear the pending data for clipboard injection. + * If no `format` is provided, all pending data formats will be cleared. + * + * @returns `undefined` + * @static + */ + ZeroClipboard.clearData = function() { + return _clearData.apply(this, _args(arguments)); + }; + /** + * Get a copy of the pending data for clipboard injection. + * If no `format` is provided, a copy of ALL pending data formats will be returned. + * + * @returns `String` or `Object` + * @static + */ + ZeroClipboard.getData = function() { + return _getData.apply(this, _args(arguments)); + }; + /** + * Sets the current HTML object that the Flash object should overlay. This will put the global + * Flash object on top of the current element; depending on the setup, this may also set the + * pending clipboard text data as well as the Flash object's wrapping element's title attribute + * based on the underlying HTML element and ZeroClipboard configuration. + * + * @returns `undefined` + * @static + */ + ZeroClipboard.focus = ZeroClipboard.activate = function() { + return _focus.apply(this, _args(arguments)); + }; + /** + * Un-overlays the Flash object. This will put the global Flash object off-screen; depending on + * the setup, this may also unset the Flash object's wrapping element's title attribute based on + * the underlying HTML element and ZeroClipboard configuration. + * + * @returns `undefined` + * @static + */ + ZeroClipboard.blur = ZeroClipboard.deactivate = function() { + return _blur.apply(this, _args(arguments)); + }; + /** + * Returns the currently focused/"activated" HTML element that the Flash object is wrapping. + * + * @returns `HTMLElement` or `null` + * @static + */ + ZeroClipboard.activeElement = function() { + return _activeElement.apply(this, _args(arguments)); + }; + if (typeof define === "function" && define.amd) { + define(function() { + return ZeroClipboard; + }); + } else if (typeof module === "object" && module && typeof module.exports === "object" && module.exports) { + module.exports = ZeroClipboard; + } else { + window.ZeroClipboard = ZeroClipboard; + } +})(function() { + return this || window; +}()); \ No newline at end of file diff --git a/bower_components/zeroclipboard/dist/ZeroClipboard.Core.min.js b/bower_components/zeroclipboard/dist/ZeroClipboard.Core.min.js new file mode 100644 index 0000000..01efd47 --- /dev/null +++ b/bower_components/zeroclipboard/dist/ZeroClipboard.Core.min.js @@ -0,0 +1,10 @@ +/*! + * ZeroClipboard + * The ZeroClipboard library provides an easy way to copy text to the clipboard using an invisible Adobe Flash movie and a JavaScript interface. + * Copyright (c) 2009-2014 Jon Rohan, James M. Greene + * Licensed MIT + * http://zeroclipboard.org/ + * v2.2.0 + */ +!function(a,b){"use strict";var c,d,e,f=a,g=f.document,h=f.navigator,i=f.setTimeout,j=f.clearTimeout,k=f.setInterval,l=f.clearInterval,m=f.getComputedStyle,n=f.encodeURIComponent,o=f.ActiveXObject,p=f.Error,q=f.Number.parseInt||f.parseInt,r=f.Number.parseFloat||f.parseFloat,s=f.Number.isNaN||f.isNaN,t=f.Date.now,u=f.Object.keys,v=f.Object.defineProperty,w=f.Object.prototype.hasOwnProperty,x=f.Array.prototype.slice,y=function(){var a=function(a){return a};if("function"==typeof f.wrap&&"function"==typeof f.unwrap)try{var b=g.createElement("div"),c=f.unwrap(b);1===b.nodeType&&c&&1===c.nodeType&&(a=f.unwrap)}catch(d){}return a}(),z=function(a){return x.call(a,0)},A=function(){var a,c,d,e,f,g,h=z(arguments),i=h[0]||{};for(a=1,c=h.length;c>a;a++)if(null!=(d=h[a]))for(e in d)w.call(d,e)&&(f=i[e],g=d[e],i!==g&&g!==b&&(i[e]=g));return i},B=function(a){var b,c,d,e;if("object"!=typeof a||null==a||"number"==typeof a.nodeType)b=a;else if("number"==typeof a.length)for(b=[],c=0,d=a.length;d>c;c++)w.call(a,c)&&(b[c]=B(a[c]));else{b={};for(e in a)w.call(a,e)&&(b[e]=B(a[e]))}return b},C=function(a,b){for(var c={},d=0,e=b.length;e>d;d++)b[d]in a&&(c[b[d]]=a[b[d]]);return c},D=function(a,b){var c={};for(var d in a)-1===b.indexOf(d)&&(c[d]=a[d]);return c},E=function(a){if(a)for(var b in a)w.call(a,b)&&delete a[b];return a},F=function(a,b){if(a&&1===a.nodeType&&a.ownerDocument&&b&&(1===b.nodeType&&b.ownerDocument&&b.ownerDocument===a.ownerDocument||9===b.nodeType&&!b.ownerDocument&&b===a.ownerDocument))do{if(a===b)return!0;a=a.parentNode}while(a);return!1},G=function(a){var b;return"string"==typeof a&&a&&(b=a.split("#")[0].split("?")[0],b=a.slice(0,a.lastIndexOf("/")+1)),b},H=function(a){var b,c;return"string"==typeof a&&a&&(c=a.match(/^(?:|[^:@]*@|.+\)@(?=http[s]?|file)|.+?\s+(?: at |@)(?:[^:\(]+ )*[\(]?)((?:http[s]?|file):\/\/[\/]?.+?\/[^:\)]*?)(?::\d+)(?::\d+)?/),c&&c[1]?b=c[1]:(c=a.match(/\)@((?:http[s]?|file):\/\/[\/]?.+?\/[^:\)]*?)(?::\d+)(?::\d+)?/),c&&c[1]&&(b=c[1]))),b},I=function(){var a,b;try{throw new p}catch(c){b=c}return b&&(a=b.sourceURL||b.fileName||H(b.stack)),a},J=function(){var a,c,d;if(g.currentScript&&(a=g.currentScript.src))return a;if(c=g.getElementsByTagName("script"),1===c.length)return c[0].src||b;if("readyState"in c[0])for(d=c.length;d--;)if("interactive"===c[d].readyState&&(a=c[d].src))return a;return"loading"===g.readyState&&(a=c[c.length-1].src)?a:(a=I())?a:b},K=function(){var a,c,d,e=g.getElementsByTagName("script");for(a=e.length;a--;){if(!(d=e[a].src)){c=null;break}if(d=G(d),null==c)c=d;else if(c!==d){c=null;break}}return c||b},L=function(){var a=G(J())||K()||"";return a+"ZeroClipboard.swf"},M=function(){return null==a.opener&&(!!a.top&&a!=a.top||!!a.parent&&a!=a.parent)}(),N={bridge:null,version:"0.0.0",pluginType:"unknown",disabled:null,outdated:null,sandboxed:null,unavailable:null,degraded:null,deactivated:null,overdue:null,ready:null},O="11.0.0",P={},Q={},R=null,S=0,T=0,U={ready:"Flash communication is established",error:{"flash-disabled":"Flash is disabled or not installed. May also be attempting to run Flash in a sandboxed iframe, which is impossible.","flash-outdated":"Flash is too outdated to support ZeroClipboard","flash-sandboxed":"Attempting to run Flash in a sandboxed iframe, which is impossible","flash-unavailable":"Flash is unable to communicate bidirectionally with JavaScript","flash-degraded":"Flash is unable to preserve data fidelity when communicating with JavaScript","flash-deactivated":"Flash is too outdated for your browser and/or is configured as click-to-activate.\nThis may also mean that the ZeroClipboard SWF object could not be loaded, so please check your `swfPath` configuration and/or network connectivity.\nMay also be attempting to run Flash in a sandboxed iframe, which is impossible.","flash-overdue":"Flash communication was established but NOT within the acceptable time limit","version-mismatch":"ZeroClipboard JS version number does not match ZeroClipboard SWF version number","clipboard-error":"At least one error was thrown while ZeroClipboard was attempting to inject your data into the clipboard","config-mismatch":"ZeroClipboard configuration does not match Flash's reality","swf-not-found":"The ZeroClipboard SWF object could not be loaded, so please check your `swfPath` configuration and/or network connectivity"}},V=["flash-unavailable","flash-degraded","flash-overdue","version-mismatch","config-mismatch","clipboard-error"],W=["flash-disabled","flash-outdated","flash-sandboxed","flash-unavailable","flash-degraded","flash-deactivated","flash-overdue"],X=new RegExp("^flash-("+W.map(function(a){return a.replace(/^flash-/,"")}).join("|")+")$"),Y=new RegExp("^flash-("+W.slice(1).map(function(a){return a.replace(/^flash-/,"")}).join("|")+")$"),Z={swfPath:L(),trustedDomains:a.location.host?[a.location.host]:[],cacheBust:!0,forceEnhancedClipboard:!1,flashLoadTimeout:3e4,autoActivate:!0,bubbleEvents:!0,containerId:"global-zeroclipboard-html-bridge",containerClass:"global-zeroclipboard-container",swfObjectId:"global-zeroclipboard-flash-bridge",hoverClass:"zeroclipboard-is-hover",activeClass:"zeroclipboard-is-active",forceHandCursor:!1,title:null,zIndex:999999999},$=function(a){if("object"==typeof a&&null!==a)for(var b in a)if(w.call(a,b))if(/^(?:forceHandCursor|title|zIndex|bubbleEvents)$/.test(b))Z[b]=a[b];else if(null==N.bridge)if("containerId"===b||"swfObjectId"===b){if(!nb(a[b]))throw new Error("The specified `"+b+"` value is not valid as an HTML4 Element ID");Z[b]=a[b]}else Z[b]=a[b];{if("string"!=typeof a||!a)return B(Z);if(w.call(Z,a))return Z[a]}},_=function(){return Tb(),{browser:C(h,["userAgent","platform","appName"]),flash:D(N,["bridge"]),zeroclipboard:{version:Vb.version,config:Vb.config()}}},ab=function(){return!!(N.disabled||N.outdated||N.sandboxed||N.unavailable||N.degraded||N.deactivated)},bb=function(a,d){var e,f,g,h={};if("string"==typeof a&&a)g=a.toLowerCase().split(/\s+/);else if("object"==typeof a&&a&&"undefined"==typeof d)for(e in a)w.call(a,e)&&"string"==typeof e&&e&&"function"==typeof a[e]&&Vb.on(e,a[e]);if(g&&g.length){for(e=0,f=g.length;f>e;e++)a=g[e].replace(/^on/,""),h[a]=!0,P[a]||(P[a]=[]),P[a].push(d);if(h.ready&&N.ready&&Vb.emit({type:"ready"}),h.error){for(e=0,f=W.length;f>e;e++)if(N[W[e].replace(/^flash-/,"")]===!0){Vb.emit({type:"error",name:W[e]});break}c!==b&&Vb.version!==c&&Vb.emit({type:"error",name:"version-mismatch",jsVersion:Vb.version,swfVersion:c})}}return Vb},cb=function(a,b){var c,d,e,f,g;if(0===arguments.length)f=u(P);else if("string"==typeof a&&a)f=a.split(/\s+/);else if("object"==typeof a&&a&&"undefined"==typeof b)for(c in a)w.call(a,c)&&"string"==typeof c&&c&&"function"==typeof a[c]&&Vb.off(c,a[c]);if(f&&f.length)for(c=0,d=f.length;d>c;c++)if(a=f[c].toLowerCase().replace(/^on/,""),g=P[a],g&&g.length)if(b)for(e=g.indexOf(b);-1!==e;)g.splice(e,1),e=g.indexOf(b,e);else g.length=0;return Vb},db=function(a){var b;return b="string"==typeof a&&a?B(P[a])||null:B(P)},eb=function(a){var b,c,d;return a=ob(a),a&&!vb(a)?"ready"===a.type&&N.overdue===!0?Vb.emit({type:"error",name:"flash-overdue"}):(b=A({},a),tb.call(this,b),"copy"===a.type&&(d=Db(Q),c=d.data,R=d.formatMap),c):void 0},fb=function(){var a=N.sandboxed;if(Tb(),"boolean"!=typeof N.ready&&(N.ready=!1),N.sandboxed!==a&&N.sandboxed===!0)N.ready=!1,Vb.emit({type:"error",name:"flash-sandboxed"});else if(!Vb.isFlashUnusable()&&null===N.bridge){var b=Z.flashLoadTimeout;"number"==typeof b&&b>=0&&(S=i(function(){"boolean"!=typeof N.deactivated&&(N.deactivated=!0),N.deactivated===!0&&Vb.emit({type:"error",name:"flash-deactivated"})},b)),N.overdue=!1,Bb()}},gb=function(){Vb.clearData(),Vb.blur(),Vb.emit("destroy"),Cb(),Vb.off()},hb=function(a,b){var c;if("object"==typeof a&&a&&"undefined"==typeof b)c=a,Vb.clearData();else{if("string"!=typeof a||!a)return;c={},c[a]=b}for(var d in c)"string"==typeof d&&d&&w.call(c,d)&&"string"==typeof c[d]&&c[d]&&(Q[d]=c[d])},ib=function(a){"undefined"==typeof a?(E(Q),R=null):"string"==typeof a&&w.call(Q,a)&&delete Q[a]},jb=function(a){return"undefined"==typeof a?B(Q):"string"==typeof a&&w.call(Q,a)?Q[a]:void 0},kb=function(a){if(a&&1===a.nodeType){d&&(Lb(d,Z.activeClass),d!==a&&Lb(d,Z.hoverClass)),d=a,Kb(a,Z.hoverClass);var b=a.getAttribute("title")||Z.title;if("string"==typeof b&&b){var c=Ab(N.bridge);c&&c.setAttribute("title",b)}var e=Z.forceHandCursor===!0||"pointer"===Mb(a,"cursor");Rb(e),Qb()}},lb=function(){var a=Ab(N.bridge);a&&(a.removeAttribute("title"),a.style.left="0px",a.style.top="-9999px",a.style.width="1px",a.style.height="1px"),d&&(Lb(d,Z.hoverClass),Lb(d,Z.activeClass),d=null)},mb=function(){return d||null},nb=function(a){return"string"==typeof a&&a&&/^[A-Za-z][A-Za-z0-9_:\-\.]*$/.test(a)},ob=function(a){var b;if("string"==typeof a&&a?(b=a,a={}):"object"==typeof a&&a&&"string"==typeof a.type&&a.type&&(b=a.type),b){b=b.toLowerCase(),!a.target&&(/^(copy|aftercopy|_click)$/.test(b)||"error"===b&&"clipboard-error"===a.name)&&(a.target=e),A(a,{type:b,target:a.target||d||null,relatedTarget:a.relatedTarget||null,currentTarget:N&&N.bridge||null,timeStamp:a.timeStamp||t()||null});var c=U[a.type];return"error"===a.type&&a.name&&c&&(c=c[a.name]),c&&(a.message=c),"ready"===a.type&&A(a,{target:null,version:N.version}),"error"===a.type&&(X.test(a.name)&&A(a,{target:null,minimumVersion:O}),Y.test(a.name)&&A(a,{version:N.version})),"copy"===a.type&&(a.clipboardData={setData:Vb.setData,clearData:Vb.clearData}),"aftercopy"===a.type&&(a=Eb(a,R)),a.target&&!a.relatedTarget&&(a.relatedTarget=pb(a.target)),qb(a)}},pb=function(a){var b=a&&a.getAttribute&&a.getAttribute("data-clipboard-target");return b?g.getElementById(b):null},qb=function(a){if(a&&/^_(?:click|mouse(?:over|out|down|up|move))$/.test(a.type)){var c=a.target,d="_mouseover"===a.type&&a.relatedTarget?a.relatedTarget:b,e="_mouseout"===a.type&&a.relatedTarget?a.relatedTarget:b,h=Nb(c),i=f.screenLeft||f.screenX||0,j=f.screenTop||f.screenY||0,k=g.body.scrollLeft+g.documentElement.scrollLeft,l=g.body.scrollTop+g.documentElement.scrollTop,m=h.left+("number"==typeof a._stageX?a._stageX:0),n=h.top+("number"==typeof a._stageY?a._stageY:0),o=m-k,p=n-l,q=i+o,r=j+p,s="number"==typeof a.movementX?a.movementX:0,t="number"==typeof a.movementY?a.movementY:0;delete a._stageX,delete a._stageY,A(a,{srcElement:c,fromElement:d,toElement:e,screenX:q,screenY:r,pageX:m,pageY:n,clientX:o,clientY:p,x:o,y:p,movementX:s,movementY:t,offsetX:0,offsetY:0,layerX:0,layerY:0})}return a},rb=function(a){var b=a&&"string"==typeof a.type&&a.type||"";return!/^(?:(?:before)?copy|destroy)$/.test(b)},sb=function(a,b,c,d){d?i(function(){a.apply(b,c)},0):a.apply(b,c)},tb=function(a){if("object"==typeof a&&a&&a.type){var b=rb(a),c=P["*"]||[],d=P[a.type]||[],e=c.concat(d);if(e&&e.length){var g,h,i,j,k,l=this;for(g=0,h=e.length;h>g;g++)i=e[g],j=l,"string"==typeof i&&"function"==typeof f[i]&&(i=f[i]),"object"==typeof i&&i&&"function"==typeof i.handleEvent&&(j=i,i=i.handleEvent),"function"==typeof i&&(k=A({},a),sb(i,j,[k],b))}return this}},ub=function(a){var b=null;return(M===!1||a&&"error"===a.type&&a.name&&-1!==V.indexOf(a.name))&&(b=!1),b},vb=function(a){var b=a.target||d||null,f="swf"===a._source;switch(delete a._source,a.type){case"error":var g="flash-sandboxed"===a.name||ub(a);"boolean"==typeof g&&(N.sandboxed=g),-1!==W.indexOf(a.name)?A(N,{disabled:"flash-disabled"===a.name,outdated:"flash-outdated"===a.name,unavailable:"flash-unavailable"===a.name,degraded:"flash-degraded"===a.name,deactivated:"flash-deactivated"===a.name,overdue:"flash-overdue"===a.name,ready:!1}):"version-mismatch"===a.name&&(c=a.swfVersion,A(N,{disabled:!1,outdated:!1,unavailable:!1,degraded:!1,deactivated:!1,overdue:!1,ready:!1})),Pb();break;case"ready":c=a.swfVersion;var h=N.deactivated===!0;A(N,{disabled:!1,outdated:!1,sandboxed:!1,unavailable:!1,degraded:!1,deactivated:!1,overdue:h,ready:!h}),Pb();break;case"beforecopy":e=b;break;case"copy":var i,j,k=a.relatedTarget;!Q["text/html"]&&!Q["text/plain"]&&k&&(j=k.value||k.outerHTML||k.innerHTML)&&(i=k.value||k.textContent||k.innerText)?(a.clipboardData.clearData(),a.clipboardData.setData("text/plain",i),j!==i&&a.clipboardData.setData("text/html",j)):!Q["text/plain"]&&a.target&&(i=a.target.getAttribute("data-clipboard-text"))&&(a.clipboardData.clearData(),a.clipboardData.setData("text/plain",i));break;case"aftercopy":wb(a),Vb.clearData(),b&&b!==Jb()&&b.focus&&b.focus();break;case"_mouseover":Vb.focus(b),Z.bubbleEvents===!0&&f&&(b&&b!==a.relatedTarget&&!F(a.relatedTarget,b)&&xb(A({},a,{type:"mouseenter",bubbles:!1,cancelable:!1})),xb(A({},a,{type:"mouseover"})));break;case"_mouseout":Vb.blur(),Z.bubbleEvents===!0&&f&&(b&&b!==a.relatedTarget&&!F(a.relatedTarget,b)&&xb(A({},a,{type:"mouseleave",bubbles:!1,cancelable:!1})),xb(A({},a,{type:"mouseout"})));break;case"_mousedown":Kb(b,Z.activeClass),Z.bubbleEvents===!0&&f&&xb(A({},a,{type:a.type.slice(1)}));break;case"_mouseup":Lb(b,Z.activeClass),Z.bubbleEvents===!0&&f&&xb(A({},a,{type:a.type.slice(1)}));break;case"_click":e=null,Z.bubbleEvents===!0&&f&&xb(A({},a,{type:a.type.slice(1)}));break;case"_mousemove":Z.bubbleEvents===!0&&f&&xb(A({},a,{type:a.type.slice(1)}))}return/^_(?:click|mouse(?:over|out|down|up|move))$/.test(a.type)?!0:void 0},wb=function(a){if(a.errors&&a.errors.length>0){var b=B(a);A(b,{type:"error",name:"clipboard-error"}),delete b.success,i(function(){Vb.emit(b)},0)}},xb=function(a){if(a&&"string"==typeof a.type&&a){var b,c=a.target||null,d=c&&c.ownerDocument||g,e={view:d.defaultView||f,canBubble:!0,cancelable:!0,detail:"click"===a.type?1:0,button:"number"==typeof a.which?a.which-1:"number"==typeof a.button?a.button:d.createEvent?0:1},h=A(e,a);c&&d.createEvent&&c.dispatchEvent&&(h=[h.type,h.canBubble,h.cancelable,h.view,h.detail,h.screenX,h.screenY,h.clientX,h.clientY,h.ctrlKey,h.altKey,h.shiftKey,h.metaKey,h.button,h.relatedTarget],b=d.createEvent("MouseEvents"),b.initMouseEvent&&(b.initMouseEvent.apply(b,h),b._source="js",c.dispatchEvent(b)))}},yb=function(){var a=Z.flashLoadTimeout;if("number"==typeof a&&a>=0){var b=Math.min(1e3,a/10),c=Z.swfObjectId+"_fallbackContent";T=k(function(){var a=g.getElementById(c);Ob(a)&&(Pb(),N.deactivated=null,Vb.emit({type:"error",name:"swf-not-found"}))},b)}},zb=function(){var a=g.createElement("div");return a.id=Z.containerId,a.className=Z.containerClass,a.style.position="absolute",a.style.left="0px",a.style.top="-9999px",a.style.width="1px",a.style.height="1px",a.style.zIndex=""+Sb(Z.zIndex),a},Ab=function(a){for(var b=a&&a.parentNode;b&&"OBJECT"===b.nodeName&&b.parentNode;)b=b.parentNode;return b||null},Bb=function(){var a,b=N.bridge,c=Ab(b);if(!b){var d=Ib(f.location.host,Z),e="never"===d?"none":"all",h=Gb(A({jsVersion:Vb.version},Z)),i=Z.swfPath+Fb(Z.swfPath,Z);c=zb();var j=g.createElement("div");c.appendChild(j),g.body.appendChild(c);var k=g.createElement("div"),l="activex"===N.pluginType;k.innerHTML='"+(l?'':"")+'
 
',b=k.firstChild,k=null,y(b).ZeroClipboard=Vb,c.replaceChild(b,j),yb()}return b||(b=g[Z.swfObjectId],b&&(a=b.length)&&(b=b[a-1]),!b&&c&&(b=c.firstChild)),N.bridge=b||null,b},Cb=function(){var a=N.bridge;if(a){var d=Ab(a);d&&("activex"===N.pluginType&&"readyState"in a?(a.style.display="none",function e(){if(4===a.readyState){for(var b in a)"function"==typeof a[b]&&(a[b]=null);a.parentNode&&a.parentNode.removeChild(a),d.parentNode&&d.parentNode.removeChild(d)}else i(e,10)}()):(a.parentNode&&a.parentNode.removeChild(a),d.parentNode&&d.parentNode.removeChild(d))),Pb(),N.ready=null,N.bridge=null,N.deactivated=null,c=b}},Db=function(a){var b={},c={};if("object"==typeof a&&a){for(var d in a)if(d&&w.call(a,d)&&"string"==typeof a[d]&&a[d])switch(d.toLowerCase()){case"text/plain":case"text":case"air:text":case"flash:text":b.text=a[d],c.text=d;break;case"text/html":case"html":case"air:html":case"flash:html":b.html=a[d],c.html=d;break;case"application/rtf":case"text/rtf":case"rtf":case"richtext":case"air:rtf":case"flash:rtf":b.rtf=a[d],c.rtf=d}return{data:b,formatMap:c}}},Eb=function(a,b){if("object"!=typeof a||!a||"object"!=typeof b||!b)return a;var c={};for(var d in a)if(w.call(a,d))if("errors"===d){c[d]=a[d]?a[d].slice():[];for(var e=0,f=c[d].length;f>e;e++)c[d][e].format=b[c[d][e].format]}else if("success"!==d&&"data"!==d)c[d]=a[d];else{c[d]={};var g=a[d];for(var h in g)h&&w.call(g,h)&&w.call(b,h)&&(c[d][b[h]]=g[h])}return c},Fb=function(a,b){var c=null==b||b&&b.cacheBust===!0;return c?(-1===a.indexOf("?")?"?":"&")+"noCache="+t():""},Gb=function(a){var b,c,d,e,g="",h=[];if(a.trustedDomains&&("string"==typeof a.trustedDomains?e=[a.trustedDomains]:"object"==typeof a.trustedDomains&&"length"in a.trustedDomains&&(e=a.trustedDomains)),e&&e.length)for(b=0,c=e.length;c>b;b++)if(w.call(e,b)&&e[b]&&"string"==typeof e[b]){if(d=Hb(e[b]),!d)continue;if("*"===d){h.length=0,h.push(d);break}h.push.apply(h,[d,"//"+d,f.location.protocol+"//"+d])}return h.length&&(g+="trustedOrigins="+n(h.join(","))),a.forceEnhancedClipboard===!0&&(g+=(g?"&":"")+"forceEnhancedClipboard=true"),"string"==typeof a.swfObjectId&&a.swfObjectId&&(g+=(g?"&":"")+"swfObjectId="+n(a.swfObjectId)),"string"==typeof a.jsVersion&&a.jsVersion&&(g+=(g?"&":"")+"jsVersion="+n(a.jsVersion)),g},Hb=function(a){if(null==a||""===a)return null;if(a=a.replace(/^\s+|\s+$/g,""),""===a)return null;var b=a.indexOf("//");a=-1===b?a:a.slice(b+2);var c=a.indexOf("/");return a=-1===c?a:-1===b||0===c?null:a.slice(0,c),a&&".swf"===a.slice(-4).toLowerCase()?null:a||null},Ib=function(){var a=function(a){var b,c,d,e=[];if("string"==typeof a&&(a=[a]),"object"!=typeof a||!a||"number"!=typeof a.length)return e;for(b=0,c=a.length;c>b;b++)if(w.call(a,b)&&(d=Hb(a[b]))){if("*"===d){e.length=0,e.push("*");break}-1===e.indexOf(d)&&e.push(d)}return e};return function(b,c){var d=Hb(c.swfPath);null===d&&(d=b);var e=a(c.trustedDomains),f=e.length;if(f>0){if(1===f&&"*"===e[0])return"always";if(-1!==e.indexOf(b))return 1===f&&b===d?"sameDomain":"always"}return"never"}}(),Jb=function(){try{return g.activeElement}catch(a){return null}},Kb=function(a,b){var c,d,e,f=[];if("string"==typeof b&&b&&(f=b.split(/\s+/)),a&&1===a.nodeType&&f.length>0)if(a.classList)for(c=0,d=f.length;d>c;c++)a.classList.add(f[c]);else if(a.hasOwnProperty("className")){for(e=" "+a.className+" ",c=0,d=f.length;d>c;c++)-1===e.indexOf(" "+f[c]+" ")&&(e+=f[c]+" ");a.className=e.replace(/^\s+|\s+$/g,"")}return a},Lb=function(a,b){var c,d,e,f=[];if("string"==typeof b&&b&&(f=b.split(/\s+/)),a&&1===a.nodeType&&f.length>0)if(a.classList&&a.classList.length>0)for(c=0,d=f.length;d>c;c++)a.classList.remove(f[c]);else if(a.className){for(e=(" "+a.className+" ").replace(/[\r\n\t]/g," "),c=0,d=f.length;d>c;c++)e=e.replace(" "+f[c]+" "," ");a.className=e.replace(/^\s+|\s+$/g,"")}return a},Mb=function(a,b){var c=m(a,null).getPropertyValue(b);return"cursor"!==b||c&&"auto"!==c||"A"!==a.nodeName?c:"pointer"},Nb=function(a){var b={left:0,top:0,width:0,height:0};if(a.getBoundingClientRect){var c=a.getBoundingClientRect(),d=f.pageXOffset,e=f.pageYOffset,h=g.documentElement.clientLeft||0,i=g.documentElement.clientTop||0,j=0,k=0;if("relative"===Mb(g.body,"position")){var l=g.body.getBoundingClientRect(),m=g.documentElement.getBoundingClientRect();j=l.left-m.left||0,k=l.top-m.top||0}b.left=c.left+d-h-j,b.top=c.top+e-i-k,b.width="width"in c?c.width:c.right-c.left,b.height="height"in c?c.height:c.bottom-c.top}return b},Ob=function(a){if(!a)return!1;var b=m(a,null),c=r(b.height)>0,d=r(b.width)>0,e=r(b.top)>=0,f=r(b.left)>=0,g=c&&d&&e&&f,h=g?null:Nb(a),i="none"!==b.display&&"collapse"!==b.visibility&&(g||!!h&&(c||h.height>0)&&(d||h.width>0)&&(e||h.top>=0)&&(f||h.left>=0));return i},Pb=function(){j(S),S=0,l(T),T=0},Qb=function(){var a;if(d&&(a=Ab(N.bridge))){var b=Nb(d);A(a.style,{width:b.width+"px",height:b.height+"px",top:b.top+"px",left:b.left+"px",zIndex:""+Sb(Z.zIndex)})}},Rb=function(a){N.ready===!0&&(N.bridge&&"function"==typeof N.bridge.setHandCursor?N.bridge.setHandCursor(a):N.ready=!1)},Sb=function(a){if(/^(?:auto|inherit)$/.test(a))return a;var b;return"number"!=typeof a||s(a)?"string"==typeof a&&(b=Sb(q(a,10))):b=a,"number"==typeof b?b:"auto"},Tb=function(b){var c,d,e,f=N.sandboxed,g=null;if(b=b===!0,M===!1)g=!1;else{try{d=a.frameElement||null}catch(h){e={name:h.name,message:h.message}}if(d&&1===d.nodeType&&"IFRAME"===d.nodeName)try{g=d.hasAttribute("sandbox")}catch(h){g=null}else{try{c=document.domain||null}catch(h){c=null}(null===c||e&&"SecurityError"===e.name&&/(^|[\s\(\[@])sandbox(es|ed|ing|[\s\.,!\)\]@]|$)/.test(e.message.toLowerCase()))&&(g=!0)}}return N.sandboxed=g,f===g||b||Ub(o),g},Ub=function(a){function b(a){var b=a.match(/[\d]+/g);return b.length=3,b.join(".")}function c(a){return!!a&&(a=a.toLowerCase())&&(/^(pepflashplayer\.dll|libpepflashplayer\.so|pepperflashplayer\.plugin)$/.test(a)||"chrome.plugin"===a.slice(-13))}function d(a){a&&(i=!0,a.version&&(l=b(a.version)),!l&&a.description&&(l=b(a.description)),a.filename&&(k=c(a.filename)))}var e,f,g,i=!1,j=!1,k=!1,l="";if(h.plugins&&h.plugins.length)e=h.plugins["Shockwave Flash"],d(e),h.plugins["Shockwave Flash 2.0"]&&(i=!0,l="2.0.0.11");else if(h.mimeTypes&&h.mimeTypes.length)g=h.mimeTypes["application/x-shockwave-flash"],e=g&&g.enabledPlugin,d(e);else if("undefined"!=typeof a){j=!0;try{f=new a("ShockwaveFlash.ShockwaveFlash.7"),i=!0,l=b(f.GetVariable("$version"))}catch(m){try{f=new a("ShockwaveFlash.ShockwaveFlash.6"),i=!0,l="6.0.21"}catch(n){try{f=new a("ShockwaveFlash.ShockwaveFlash"),i=!0,l=b(f.GetVariable("$version"))}catch(o){j=!1}}}}N.disabled=i!==!0,N.outdated=l&&r(l)= 0) {\n _flashCheckTimeout = _setTimeout(function() {\n if (typeof _flashState.deactivated !== \"boolean\") {\n _flashState.deactivated = true;\n }\n if (_flashState.deactivated === true) {\n ZeroClipboard.emit({\n type: \"error\",\n name: \"flash-deactivated\"\n });\n }\n }, maxWait);\n }\n _flashState.overdue = false;\n _embedSwf();\n }\n };\n /**\n * The underlying implementation of `ZeroClipboard.destroy`.\n * @private\n */\n var _destroy = function() {\n ZeroClipboard.clearData();\n ZeroClipboard.blur();\n ZeroClipboard.emit(\"destroy\");\n _unembedSwf();\n ZeroClipboard.off();\n };\n /**\n * The underlying implementation of `ZeroClipboard.setData`.\n * @private\n */\n var _setData = function(format, data) {\n var dataObj;\n if (typeof format === \"object\" && format && typeof data === \"undefined\") {\n dataObj = format;\n ZeroClipboard.clearData();\n } else if (typeof format === \"string\" && format) {\n dataObj = {};\n dataObj[format] = data;\n } else {\n return;\n }\n for (var dataFormat in dataObj) {\n if (typeof dataFormat === \"string\" && dataFormat && _hasOwn.call(dataObj, dataFormat) && typeof dataObj[dataFormat] === \"string\" && dataObj[dataFormat]) {\n _clipData[dataFormat] = dataObj[dataFormat];\n }\n }\n };\n /**\n * The underlying implementation of `ZeroClipboard.clearData`.\n * @private\n */\n var _clearData = function(format) {\n if (typeof format === \"undefined\") {\n _deleteOwnProperties(_clipData);\n _clipDataFormatMap = null;\n } else if (typeof format === \"string\" && _hasOwn.call(_clipData, format)) {\n delete _clipData[format];\n }\n };\n /**\n * The underlying implementation of `ZeroClipboard.getData`.\n * @private\n */\n var _getData = function(format) {\n if (typeof format === \"undefined\") {\n return _deepCopy(_clipData);\n } else if (typeof format === \"string\" && _hasOwn.call(_clipData, format)) {\n return _clipData[format];\n }\n };\n /**\n * The underlying implementation of `ZeroClipboard.focus`/`ZeroClipboard.activate`.\n * @private\n */\n var _focus = function(element) {\n if (!(element && element.nodeType === 1)) {\n return;\n }\n if (_currentElement) {\n _removeClass(_currentElement, _globalConfig.activeClass);\n if (_currentElement !== element) {\n _removeClass(_currentElement, _globalConfig.hoverClass);\n }\n }\n _currentElement = element;\n _addClass(element, _globalConfig.hoverClass);\n var newTitle = element.getAttribute(\"title\") || _globalConfig.title;\n if (typeof newTitle === \"string\" && newTitle) {\n var htmlBridge = _getHtmlBridge(_flashState.bridge);\n if (htmlBridge) {\n htmlBridge.setAttribute(\"title\", newTitle);\n }\n }\n var useHandCursor = _globalConfig.forceHandCursor === true || _getStyle(element, \"cursor\") === \"pointer\";\n _setHandCursor(useHandCursor);\n _reposition();\n };\n /**\n * The underlying implementation of `ZeroClipboard.blur`/`ZeroClipboard.deactivate`.\n * @private\n */\n var _blur = function() {\n var htmlBridge = _getHtmlBridge(_flashState.bridge);\n if (htmlBridge) {\n htmlBridge.removeAttribute(\"title\");\n htmlBridge.style.left = \"0px\";\n htmlBridge.style.top = \"-9999px\";\n htmlBridge.style.width = \"1px\";\n htmlBridge.style.height = \"1px\";\n }\n if (_currentElement) {\n _removeClass(_currentElement, _globalConfig.hoverClass);\n _removeClass(_currentElement, _globalConfig.activeClass);\n _currentElement = null;\n }\n };\n /**\n * The underlying implementation of `ZeroClipboard.activeElement`.\n * @private\n */\n var _activeElement = function() {\n return _currentElement || null;\n };\n /**\n * Check if a value is a valid HTML4 `ID` or `Name` token.\n * @private\n */\n var _isValidHtml4Id = function(id) {\n return typeof id === \"string\" && id && /^[A-Za-z][A-Za-z0-9_:\\-\\.]*$/.test(id);\n };\n /**\n * Create or update an `event` object, based on the `eventType`.\n * @private\n */\n var _createEvent = function(event) {\n var eventType;\n if (typeof event === \"string\" && event) {\n eventType = event;\n event = {};\n } else if (typeof event === \"object\" && event && typeof event.type === \"string\" && event.type) {\n eventType = event.type;\n }\n if (!eventType) {\n return;\n }\n eventType = eventType.toLowerCase();\n if (!event.target && (/^(copy|aftercopy|_click)$/.test(eventType) || eventType === \"error\" && event.name === \"clipboard-error\")) {\n event.target = _copyTarget;\n }\n _extend(event, {\n type: eventType,\n target: event.target || _currentElement || null,\n relatedTarget: event.relatedTarget || null,\n currentTarget: _flashState && _flashState.bridge || null,\n timeStamp: event.timeStamp || _now() || null\n });\n var msg = _eventMessages[event.type];\n if (event.type === \"error\" && event.name && msg) {\n msg = msg[event.name];\n }\n if (msg) {\n event.message = msg;\n }\n if (event.type === \"ready\") {\n _extend(event, {\n target: null,\n version: _flashState.version\n });\n }\n if (event.type === \"error\") {\n if (_flashStateErrorNameMatchingRegex.test(event.name)) {\n _extend(event, {\n target: null,\n minimumVersion: _minimumFlashVersion\n });\n }\n if (_flashStateEnabledErrorNameMatchingRegex.test(event.name)) {\n _extend(event, {\n version: _flashState.version\n });\n }\n }\n if (event.type === \"copy\") {\n event.clipboardData = {\n setData: ZeroClipboard.setData,\n clearData: ZeroClipboard.clearData\n };\n }\n if (event.type === \"aftercopy\") {\n event = _mapClipResultsFromFlash(event, _clipDataFormatMap);\n }\n if (event.target && !event.relatedTarget) {\n event.relatedTarget = _getRelatedTarget(event.target);\n }\n return _addMouseData(event);\n };\n /**\n * Get a relatedTarget from the target's `data-clipboard-target` attribute\n * @private\n */\n var _getRelatedTarget = function(targetEl) {\n var relatedTargetId = targetEl && targetEl.getAttribute && targetEl.getAttribute(\"data-clipboard-target\");\n return relatedTargetId ? _document.getElementById(relatedTargetId) : null;\n };\n /**\n * Add element and position data to `MouseEvent` instances\n * @private\n */\n var _addMouseData = function(event) {\n if (event && /^_(?:click|mouse(?:over|out|down|up|move))$/.test(event.type)) {\n var srcElement = event.target;\n var fromElement = event.type === \"_mouseover\" && event.relatedTarget ? event.relatedTarget : undefined;\n var toElement = event.type === \"_mouseout\" && event.relatedTarget ? event.relatedTarget : undefined;\n var pos = _getElementPosition(srcElement);\n var screenLeft = _window.screenLeft || _window.screenX || 0;\n var screenTop = _window.screenTop || _window.screenY || 0;\n var scrollLeft = _document.body.scrollLeft + _document.documentElement.scrollLeft;\n var scrollTop = _document.body.scrollTop + _document.documentElement.scrollTop;\n var pageX = pos.left + (typeof event._stageX === \"number\" ? event._stageX : 0);\n var pageY = pos.top + (typeof event._stageY === \"number\" ? event._stageY : 0);\n var clientX = pageX - scrollLeft;\n var clientY = pageY - scrollTop;\n var screenX = screenLeft + clientX;\n var screenY = screenTop + clientY;\n var moveX = typeof event.movementX === \"number\" ? event.movementX : 0;\n var moveY = typeof event.movementY === \"number\" ? event.movementY : 0;\n delete event._stageX;\n delete event._stageY;\n _extend(event, {\n srcElement: srcElement,\n fromElement: fromElement,\n toElement: toElement,\n screenX: screenX,\n screenY: screenY,\n pageX: pageX,\n pageY: pageY,\n clientX: clientX,\n clientY: clientY,\n x: clientX,\n y: clientY,\n movementX: moveX,\n movementY: moveY,\n offsetX: 0,\n offsetY: 0,\n layerX: 0,\n layerY: 0\n });\n }\n return event;\n };\n /**\n * Determine if an event's registered handlers should be execute synchronously or asynchronously.\n *\n * @returns {boolean}\n * @private\n */\n var _shouldPerformAsync = function(event) {\n var eventType = event && typeof event.type === \"string\" && event.type || \"\";\n return !/^(?:(?:before)?copy|destroy)$/.test(eventType);\n };\n /**\n * Control if a callback should be executed asynchronously or not.\n *\n * @returns `undefined`\n * @private\n */\n var _dispatchCallback = function(func, context, args, async) {\n if (async) {\n _setTimeout(function() {\n func.apply(context, args);\n }, 0);\n } else {\n func.apply(context, args);\n }\n };\n /**\n * Handle the actual dispatching of events to client instances.\n *\n * @returns `undefined`\n * @private\n */\n var _dispatchCallbacks = function(event) {\n if (!(typeof event === \"object\" && event && event.type)) {\n return;\n }\n var async = _shouldPerformAsync(event);\n var wildcardTypeHandlers = _handlers[\"*\"] || [];\n var specificTypeHandlers = _handlers[event.type] || [];\n var handlers = wildcardTypeHandlers.concat(specificTypeHandlers);\n if (handlers && handlers.length) {\n var i, len, func, context, eventCopy, originalContext = this;\n for (i = 0, len = handlers.length; i < len; i++) {\n func = handlers[i];\n context = originalContext;\n if (typeof func === \"string\" && typeof _window[func] === \"function\") {\n func = _window[func];\n }\n if (typeof func === \"object\" && func && typeof func.handleEvent === \"function\") {\n context = func;\n func = func.handleEvent;\n }\n if (typeof func === \"function\") {\n eventCopy = _extend({}, event);\n _dispatchCallback(func, context, [ eventCopy ], async);\n }\n }\n }\n return this;\n };\n /**\n * Check an `error` event's `name` property to see if Flash has\n * already loaded, which rules out possible `iframe` sandboxing.\n * @private\n */\n var _getSandboxStatusFromErrorEvent = function(event) {\n var isSandboxed = null;\n if (_pageIsFramed === false || event && event.type === \"error\" && event.name && _errorsThatOnlyOccurAfterFlashLoads.indexOf(event.name) !== -1) {\n isSandboxed = false;\n }\n return isSandboxed;\n };\n /**\n * Preprocess any special behaviors, reactions, or state changes after receiving this event.\n * Executes only once per event emitted, NOT once per client.\n * @private\n */\n var _preprocessEvent = function(event) {\n var element = event.target || _currentElement || null;\n var sourceIsSwf = event._source === \"swf\";\n delete event._source;\n switch (event.type) {\n case \"error\":\n var isSandboxed = event.name === \"flash-sandboxed\" || _getSandboxStatusFromErrorEvent(event);\n if (typeof isSandboxed === \"boolean\") {\n _flashState.sandboxed = isSandboxed;\n }\n if (_flashStateErrorNames.indexOf(event.name) !== -1) {\n _extend(_flashState, {\n disabled: event.name === \"flash-disabled\",\n outdated: event.name === \"flash-outdated\",\n unavailable: event.name === \"flash-unavailable\",\n degraded: event.name === \"flash-degraded\",\n deactivated: event.name === \"flash-deactivated\",\n overdue: event.name === \"flash-overdue\",\n ready: false\n });\n } else if (event.name === \"version-mismatch\") {\n _zcSwfVersion = event.swfVersion;\n _extend(_flashState, {\n disabled: false,\n outdated: false,\n unavailable: false,\n degraded: false,\n deactivated: false,\n overdue: false,\n ready: false\n });\n }\n _clearTimeoutsAndPolling();\n break;\n\n case \"ready\":\n _zcSwfVersion = event.swfVersion;\n var wasDeactivated = _flashState.deactivated === true;\n _extend(_flashState, {\n disabled: false,\n outdated: false,\n sandboxed: false,\n unavailable: false,\n degraded: false,\n deactivated: false,\n overdue: wasDeactivated,\n ready: !wasDeactivated\n });\n _clearTimeoutsAndPolling();\n break;\n\n case \"beforecopy\":\n _copyTarget = element;\n break;\n\n case \"copy\":\n var textContent, htmlContent, targetEl = event.relatedTarget;\n if (!(_clipData[\"text/html\"] || _clipData[\"text/plain\"]) && targetEl && (htmlContent = targetEl.value || targetEl.outerHTML || targetEl.innerHTML) && (textContent = targetEl.value || targetEl.textContent || targetEl.innerText)) {\n event.clipboardData.clearData();\n event.clipboardData.setData(\"text/plain\", textContent);\n if (htmlContent !== textContent) {\n event.clipboardData.setData(\"text/html\", htmlContent);\n }\n } else if (!_clipData[\"text/plain\"] && event.target && (textContent = event.target.getAttribute(\"data-clipboard-text\"))) {\n event.clipboardData.clearData();\n event.clipboardData.setData(\"text/plain\", textContent);\n }\n break;\n\n case \"aftercopy\":\n _queueEmitClipboardErrors(event);\n ZeroClipboard.clearData();\n if (element && element !== _safeActiveElement() && element.focus) {\n element.focus();\n }\n break;\n\n case \"_mouseover\":\n ZeroClipboard.focus(element);\n if (_globalConfig.bubbleEvents === true && sourceIsSwf) {\n if (element && element !== event.relatedTarget && !_containedBy(event.relatedTarget, element)) {\n _fireMouseEvent(_extend({}, event, {\n type: \"mouseenter\",\n bubbles: false,\n cancelable: false\n }));\n }\n _fireMouseEvent(_extend({}, event, {\n type: \"mouseover\"\n }));\n }\n break;\n\n case \"_mouseout\":\n ZeroClipboard.blur();\n if (_globalConfig.bubbleEvents === true && sourceIsSwf) {\n if (element && element !== event.relatedTarget && !_containedBy(event.relatedTarget, element)) {\n _fireMouseEvent(_extend({}, event, {\n type: \"mouseleave\",\n bubbles: false,\n cancelable: false\n }));\n }\n _fireMouseEvent(_extend({}, event, {\n type: \"mouseout\"\n }));\n }\n break;\n\n case \"_mousedown\":\n _addClass(element, _globalConfig.activeClass);\n if (_globalConfig.bubbleEvents === true && sourceIsSwf) {\n _fireMouseEvent(_extend({}, event, {\n type: event.type.slice(1)\n }));\n }\n break;\n\n case \"_mouseup\":\n _removeClass(element, _globalConfig.activeClass);\n if (_globalConfig.bubbleEvents === true && sourceIsSwf) {\n _fireMouseEvent(_extend({}, event, {\n type: event.type.slice(1)\n }));\n }\n break;\n\n case \"_click\":\n _copyTarget = null;\n if (_globalConfig.bubbleEvents === true && sourceIsSwf) {\n _fireMouseEvent(_extend({}, event, {\n type: event.type.slice(1)\n }));\n }\n break;\n\n case \"_mousemove\":\n if (_globalConfig.bubbleEvents === true && sourceIsSwf) {\n _fireMouseEvent(_extend({}, event, {\n type: event.type.slice(1)\n }));\n }\n break;\n }\n if (/^_(?:click|mouse(?:over|out|down|up|move))$/.test(event.type)) {\n return true;\n }\n };\n /**\n * Check an \"aftercopy\" event for clipboard errors and emit a corresponding \"error\" event.\n * @private\n */\n var _queueEmitClipboardErrors = function(aftercopyEvent) {\n if (aftercopyEvent.errors && aftercopyEvent.errors.length > 0) {\n var errorEvent = _deepCopy(aftercopyEvent);\n _extend(errorEvent, {\n type: \"error\",\n name: \"clipboard-error\"\n });\n delete errorEvent.success;\n _setTimeout(function() {\n ZeroClipboard.emit(errorEvent);\n }, 0);\n }\n };\n /**\n * Dispatch a synthetic MouseEvent.\n *\n * @returns `undefined`\n * @private\n */\n var _fireMouseEvent = function(event) {\n if (!(event && typeof event.type === \"string\" && event)) {\n return;\n }\n var e, target = event.target || null, doc = target && target.ownerDocument || _document, defaults = {\n view: doc.defaultView || _window,\n canBubble: true,\n cancelable: true,\n detail: event.type === \"click\" ? 1 : 0,\n button: typeof event.which === \"number\" ? event.which - 1 : typeof event.button === \"number\" ? event.button : doc.createEvent ? 0 : 1\n }, args = _extend(defaults, event);\n if (!target) {\n return;\n }\n if (doc.createEvent && target.dispatchEvent) {\n args = [ args.type, args.canBubble, args.cancelable, args.view, args.detail, args.screenX, args.screenY, args.clientX, args.clientY, args.ctrlKey, args.altKey, args.shiftKey, args.metaKey, args.button, args.relatedTarget ];\n e = doc.createEvent(\"MouseEvents\");\n if (e.initMouseEvent) {\n e.initMouseEvent.apply(e, args);\n e._source = \"js\";\n target.dispatchEvent(e);\n }\n }\n };\n /**\n * Continuously poll the DOM until either:\n * (a) the fallback content becomes visible, or\n * (b) we receive an event from SWF (handled elsewhere)\n *\n * IMPORTANT:\n * This is NOT a necessary check but it can result in significantly faster\n * detection of bad `swfPath` configuration and/or network/server issues [in\n * supported browsers] than waiting for the entire `flashLoadTimeout` duration\n * to elapse before detecting that the SWF cannot be loaded. The detection\n * duration can be anywhere from 10-30 times faster [in supported browsers] by\n * using this approach.\n *\n * @returns `undefined`\n * @private\n */\n var _watchForSwfFallbackContent = function() {\n var maxWait = _globalConfig.flashLoadTimeout;\n if (typeof maxWait === \"number\" && maxWait >= 0) {\n var pollWait = Math.min(1e3, maxWait / 10);\n var fallbackContentId = _globalConfig.swfObjectId + \"_fallbackContent\";\n _swfFallbackCheckInterval = _setInterval(function() {\n var el = _document.getElementById(fallbackContentId);\n if (_isElementVisible(el)) {\n _clearTimeoutsAndPolling();\n _flashState.deactivated = null;\n ZeroClipboard.emit({\n type: \"error\",\n name: \"swf-not-found\"\n });\n }\n }, pollWait);\n }\n };\n /**\n * Create the HTML bridge element to embed the Flash object into.\n * @private\n */\n var _createHtmlBridge = function() {\n var container = _document.createElement(\"div\");\n container.id = _globalConfig.containerId;\n container.className = _globalConfig.containerClass;\n container.style.position = \"absolute\";\n container.style.left = \"0px\";\n container.style.top = \"-9999px\";\n container.style.width = \"1px\";\n container.style.height = \"1px\";\n container.style.zIndex = \"\" + _getSafeZIndex(_globalConfig.zIndex);\n return container;\n };\n /**\n * Get the HTML element container that wraps the Flash bridge object/element.\n * @private\n */\n var _getHtmlBridge = function(flashBridge) {\n var htmlBridge = flashBridge && flashBridge.parentNode;\n while (htmlBridge && htmlBridge.nodeName === \"OBJECT\" && htmlBridge.parentNode) {\n htmlBridge = htmlBridge.parentNode;\n }\n return htmlBridge || null;\n };\n /**\n * Create the SWF object.\n *\n * @returns The SWF object reference.\n * @private\n */\n var _embedSwf = function() {\n var len, flashBridge = _flashState.bridge, container = _getHtmlBridge(flashBridge);\n if (!flashBridge) {\n var allowScriptAccess = _determineScriptAccess(_window.location.host, _globalConfig);\n var allowNetworking = allowScriptAccess === \"never\" ? \"none\" : \"all\";\n var flashvars = _vars(_extend({\n jsVersion: ZeroClipboard.version\n }, _globalConfig));\n var swfUrl = _globalConfig.swfPath + _cacheBust(_globalConfig.swfPath, _globalConfig);\n container = _createHtmlBridge();\n var divToBeReplaced = _document.createElement(\"div\");\n container.appendChild(divToBeReplaced);\n _document.body.appendChild(container);\n var tmpDiv = _document.createElement(\"div\");\n var usingActiveX = _flashState.pluginType === \"activex\";\n tmpDiv.innerHTML = '\" + (usingActiveX ? '' : \"\") + '' + '' + '' + '' + '' + '
 
' + \"
\";\n flashBridge = tmpDiv.firstChild;\n tmpDiv = null;\n _unwrap(flashBridge).ZeroClipboard = ZeroClipboard;\n container.replaceChild(flashBridge, divToBeReplaced);\n _watchForSwfFallbackContent();\n }\n if (!flashBridge) {\n flashBridge = _document[_globalConfig.swfObjectId];\n if (flashBridge && (len = flashBridge.length)) {\n flashBridge = flashBridge[len - 1];\n }\n if (!flashBridge && container) {\n flashBridge = container.firstChild;\n }\n }\n _flashState.bridge = flashBridge || null;\n return flashBridge;\n };\n /**\n * Destroy the SWF object.\n * @private\n */\n var _unembedSwf = function() {\n var flashBridge = _flashState.bridge;\n if (flashBridge) {\n var htmlBridge = _getHtmlBridge(flashBridge);\n if (htmlBridge) {\n if (_flashState.pluginType === \"activex\" && \"readyState\" in flashBridge) {\n flashBridge.style.display = \"none\";\n (function removeSwfFromIE() {\n if (flashBridge.readyState === 4) {\n for (var prop in flashBridge) {\n if (typeof flashBridge[prop] === \"function\") {\n flashBridge[prop] = null;\n }\n }\n if (flashBridge.parentNode) {\n flashBridge.parentNode.removeChild(flashBridge);\n }\n if (htmlBridge.parentNode) {\n htmlBridge.parentNode.removeChild(htmlBridge);\n }\n } else {\n _setTimeout(removeSwfFromIE, 10);\n }\n })();\n } else {\n if (flashBridge.parentNode) {\n flashBridge.parentNode.removeChild(flashBridge);\n }\n if (htmlBridge.parentNode) {\n htmlBridge.parentNode.removeChild(htmlBridge);\n }\n }\n }\n _clearTimeoutsAndPolling();\n _flashState.ready = null;\n _flashState.bridge = null;\n _flashState.deactivated = null;\n _zcSwfVersion = undefined;\n }\n };\n /**\n * Map the data format names of the \"clipData\" to Flash-friendly names.\n *\n * @returns A new transformed object.\n * @private\n */\n var _mapClipDataToFlash = function(clipData) {\n var newClipData = {}, formatMap = {};\n if (!(typeof clipData === \"object\" && clipData)) {\n return;\n }\n for (var dataFormat in clipData) {\n if (dataFormat && _hasOwn.call(clipData, dataFormat) && typeof clipData[dataFormat] === \"string\" && clipData[dataFormat]) {\n switch (dataFormat.toLowerCase()) {\n case \"text/plain\":\n case \"text\":\n case \"air:text\":\n case \"flash:text\":\n newClipData.text = clipData[dataFormat];\n formatMap.text = dataFormat;\n break;\n\n case \"text/html\":\n case \"html\":\n case \"air:html\":\n case \"flash:html\":\n newClipData.html = clipData[dataFormat];\n formatMap.html = dataFormat;\n break;\n\n case \"application/rtf\":\n case \"text/rtf\":\n case \"rtf\":\n case \"richtext\":\n case \"air:rtf\":\n case \"flash:rtf\":\n newClipData.rtf = clipData[dataFormat];\n formatMap.rtf = dataFormat;\n break;\n\n default:\n break;\n }\n }\n }\n return {\n data: newClipData,\n formatMap: formatMap\n };\n };\n /**\n * Map the data format names from Flash-friendly names back to their original \"clipData\" names (via a format mapping).\n *\n * @returns A new transformed object.\n * @private\n */\n var _mapClipResultsFromFlash = function(clipResults, formatMap) {\n if (!(typeof clipResults === \"object\" && clipResults && typeof formatMap === \"object\" && formatMap)) {\n return clipResults;\n }\n var newResults = {};\n for (var prop in clipResults) {\n if (_hasOwn.call(clipResults, prop)) {\n if (prop === \"errors\") {\n newResults[prop] = clipResults[prop] ? clipResults[prop].slice() : [];\n for (var i = 0, len = newResults[prop].length; i < len; i++) {\n newResults[prop][i].format = formatMap[newResults[prop][i].format];\n }\n } else if (prop !== \"success\" && prop !== \"data\") {\n newResults[prop] = clipResults[prop];\n } else {\n newResults[prop] = {};\n var tmpHash = clipResults[prop];\n for (var dataFormat in tmpHash) {\n if (dataFormat && _hasOwn.call(tmpHash, dataFormat) && _hasOwn.call(formatMap, dataFormat)) {\n newResults[prop][formatMap[dataFormat]] = tmpHash[dataFormat];\n }\n }\n }\n }\n }\n return newResults;\n };\n /**\n * Will look at a path, and will create a \"?noCache={time}\" or \"&noCache={time}\"\n * query param string to return. Does NOT append that string to the original path.\n * This is useful because ExternalInterface often breaks when a Flash SWF is cached.\n *\n * @returns The `noCache` query param with necessary \"?\"/\"&\" prefix.\n * @private\n */\n var _cacheBust = function(path, options) {\n var cacheBust = options == null || options && options.cacheBust === true;\n if (cacheBust) {\n return (path.indexOf(\"?\") === -1 ? \"?\" : \"&\") + \"noCache=\" + _now();\n } else {\n return \"\";\n }\n };\n /**\n * Creates a query string for the FlashVars param.\n * Does NOT include the cache-busting query param.\n *\n * @returns FlashVars query string\n * @private\n */\n var _vars = function(options) {\n var i, len, domain, domains, str = \"\", trustedOriginsExpanded = [];\n if (options.trustedDomains) {\n if (typeof options.trustedDomains === \"string\") {\n domains = [ options.trustedDomains ];\n } else if (typeof options.trustedDomains === \"object\" && \"length\" in options.trustedDomains) {\n domains = options.trustedDomains;\n }\n }\n if (domains && domains.length) {\n for (i = 0, len = domains.length; i < len; i++) {\n if (_hasOwn.call(domains, i) && domains[i] && typeof domains[i] === \"string\") {\n domain = _extractDomain(domains[i]);\n if (!domain) {\n continue;\n }\n if (domain === \"*\") {\n trustedOriginsExpanded.length = 0;\n trustedOriginsExpanded.push(domain);\n break;\n }\n trustedOriginsExpanded.push.apply(trustedOriginsExpanded, [ domain, \"//\" + domain, _window.location.protocol + \"//\" + domain ]);\n }\n }\n }\n if (trustedOriginsExpanded.length) {\n str += \"trustedOrigins=\" + _encodeURIComponent(trustedOriginsExpanded.join(\",\"));\n }\n if (options.forceEnhancedClipboard === true) {\n str += (str ? \"&\" : \"\") + \"forceEnhancedClipboard=true\";\n }\n if (typeof options.swfObjectId === \"string\" && options.swfObjectId) {\n str += (str ? \"&\" : \"\") + \"swfObjectId=\" + _encodeURIComponent(options.swfObjectId);\n }\n if (typeof options.jsVersion === \"string\" && options.jsVersion) {\n str += (str ? \"&\" : \"\") + \"jsVersion=\" + _encodeURIComponent(options.jsVersion);\n }\n return str;\n };\n /**\n * Extract the domain (e.g. \"github.com\") from an origin (e.g. \"https://github.com\") or\n * URL (e.g. \"https://github.com/zeroclipboard/zeroclipboard/\").\n *\n * @returns the domain\n * @private\n */\n var _extractDomain = function(originOrUrl) {\n if (originOrUrl == null || originOrUrl === \"\") {\n return null;\n }\n originOrUrl = originOrUrl.replace(/^\\s+|\\s+$/g, \"\");\n if (originOrUrl === \"\") {\n return null;\n }\n var protocolIndex = originOrUrl.indexOf(\"//\");\n originOrUrl = protocolIndex === -1 ? originOrUrl : originOrUrl.slice(protocolIndex + 2);\n var pathIndex = originOrUrl.indexOf(\"/\");\n originOrUrl = pathIndex === -1 ? originOrUrl : protocolIndex === -1 || pathIndex === 0 ? null : originOrUrl.slice(0, pathIndex);\n if (originOrUrl && originOrUrl.slice(-4).toLowerCase() === \".swf\") {\n return null;\n }\n return originOrUrl || null;\n };\n /**\n * Set `allowScriptAccess` based on `trustedDomains` and `window.location.host` vs. `swfPath`.\n *\n * @returns The appropriate script access level.\n * @private\n */\n var _determineScriptAccess = function() {\n var _extractAllDomains = function(origins) {\n var i, len, tmp, resultsArray = [];\n if (typeof origins === \"string\") {\n origins = [ origins ];\n }\n if (!(typeof origins === \"object\" && origins && typeof origins.length === \"number\")) {\n return resultsArray;\n }\n for (i = 0, len = origins.length; i < len; i++) {\n if (_hasOwn.call(origins, i) && (tmp = _extractDomain(origins[i]))) {\n if (tmp === \"*\") {\n resultsArray.length = 0;\n resultsArray.push(\"*\");\n break;\n }\n if (resultsArray.indexOf(tmp) === -1) {\n resultsArray.push(tmp);\n }\n }\n }\n return resultsArray;\n };\n return function(currentDomain, configOptions) {\n var swfDomain = _extractDomain(configOptions.swfPath);\n if (swfDomain === null) {\n swfDomain = currentDomain;\n }\n var trustedDomains = _extractAllDomains(configOptions.trustedDomains);\n var len = trustedDomains.length;\n if (len > 0) {\n if (len === 1 && trustedDomains[0] === \"*\") {\n return \"always\";\n }\n if (trustedDomains.indexOf(currentDomain) !== -1) {\n if (len === 1 && currentDomain === swfDomain) {\n return \"sameDomain\";\n }\n return \"always\";\n }\n }\n return \"never\";\n };\n }();\n /**\n * Get the currently active/focused DOM element.\n *\n * @returns the currently active/focused element, or `null`\n * @private\n */\n var _safeActiveElement = function() {\n try {\n return _document.activeElement;\n } catch (err) {\n return null;\n }\n };\n /**\n * Add a class to an element, if it doesn't already have it.\n *\n * @returns The element, with its new class added.\n * @private\n */\n var _addClass = function(element, value) {\n var c, cl, className, classNames = [];\n if (typeof value === \"string\" && value) {\n classNames = value.split(/\\s+/);\n }\n if (element && element.nodeType === 1 && classNames.length > 0) {\n if (element.classList) {\n for (c = 0, cl = classNames.length; c < cl; c++) {\n element.classList.add(classNames[c]);\n }\n } else if (element.hasOwnProperty(\"className\")) {\n className = \" \" + element.className + \" \";\n for (c = 0, cl = classNames.length; c < cl; c++) {\n if (className.indexOf(\" \" + classNames[c] + \" \") === -1) {\n className += classNames[c] + \" \";\n }\n }\n element.className = className.replace(/^\\s+|\\s+$/g, \"\");\n }\n }\n return element;\n };\n /**\n * Remove a class from an element, if it has it.\n *\n * @returns The element, with its class removed.\n * @private\n */\n var _removeClass = function(element, value) {\n var c, cl, className, classNames = [];\n if (typeof value === \"string\" && value) {\n classNames = value.split(/\\s+/);\n }\n if (element && element.nodeType === 1 && classNames.length > 0) {\n if (element.classList && element.classList.length > 0) {\n for (c = 0, cl = classNames.length; c < cl; c++) {\n element.classList.remove(classNames[c]);\n }\n } else if (element.className) {\n className = (\" \" + element.className + \" \").replace(/[\\r\\n\\t]/g, \" \");\n for (c = 0, cl = classNames.length; c < cl; c++) {\n className = className.replace(\" \" + classNames[c] + \" \", \" \");\n }\n element.className = className.replace(/^\\s+|\\s+$/g, \"\");\n }\n }\n return element;\n };\n /**\n * Attempt to interpret the element's CSS styling. If `prop` is `\"cursor\"`,\n * then we assume that it should be a hand (\"pointer\") cursor if the element\n * is an anchor element (\"a\" tag).\n *\n * @returns The computed style property.\n * @private\n */\n var _getStyle = function(el, prop) {\n var value = _getComputedStyle(el, null).getPropertyValue(prop);\n if (prop === \"cursor\") {\n if (!value || value === \"auto\") {\n if (el.nodeName === \"A\") {\n return \"pointer\";\n }\n }\n }\n return value;\n };\n /**\n * Get the absolutely positioned coordinates of a DOM element.\n *\n * @returns Object containing the element's position, width, and height.\n * @private\n */\n var _getElementPosition = function(el) {\n var pos = {\n left: 0,\n top: 0,\n width: 0,\n height: 0\n };\n if (el.getBoundingClientRect) {\n var elRect = el.getBoundingClientRect();\n var pageXOffset = _window.pageXOffset;\n var pageYOffset = _window.pageYOffset;\n var leftBorderWidth = _document.documentElement.clientLeft || 0;\n var topBorderWidth = _document.documentElement.clientTop || 0;\n var leftBodyOffset = 0;\n var topBodyOffset = 0;\n if (_getStyle(_document.body, \"position\") === \"relative\") {\n var bodyRect = _document.body.getBoundingClientRect();\n var htmlRect = _document.documentElement.getBoundingClientRect();\n leftBodyOffset = bodyRect.left - htmlRect.left || 0;\n topBodyOffset = bodyRect.top - htmlRect.top || 0;\n }\n pos.left = elRect.left + pageXOffset - leftBorderWidth - leftBodyOffset;\n pos.top = elRect.top + pageYOffset - topBorderWidth - topBodyOffset;\n pos.width = \"width\" in elRect ? elRect.width : elRect.right - elRect.left;\n pos.height = \"height\" in elRect ? elRect.height : elRect.bottom - elRect.top;\n }\n return pos;\n };\n /**\n * Determine is an element is visible somewhere within the document (page).\n *\n * @returns Boolean\n * @private\n */\n var _isElementVisible = function(el) {\n if (!el) {\n return false;\n }\n var styles = _getComputedStyle(el, null);\n var hasCssHeight = _parseFloat(styles.height) > 0;\n var hasCssWidth = _parseFloat(styles.width) > 0;\n var hasCssTop = _parseFloat(styles.top) >= 0;\n var hasCssLeft = _parseFloat(styles.left) >= 0;\n var cssKnows = hasCssHeight && hasCssWidth && hasCssTop && hasCssLeft;\n var rect = cssKnows ? null : _getElementPosition(el);\n var isVisible = styles.display !== \"none\" && styles.visibility !== \"collapse\" && (cssKnows || !!rect && (hasCssHeight || rect.height > 0) && (hasCssWidth || rect.width > 0) && (hasCssTop || rect.top >= 0) && (hasCssLeft || rect.left >= 0));\n return isVisible;\n };\n /**\n * Clear all existing timeouts and interval polling delegates.\n *\n * @returns `undefined`\n * @private\n */\n var _clearTimeoutsAndPolling = function() {\n _clearTimeout(_flashCheckTimeout);\n _flashCheckTimeout = 0;\n _clearInterval(_swfFallbackCheckInterval);\n _swfFallbackCheckInterval = 0;\n };\n /**\n * Reposition the Flash object to cover the currently activated element.\n *\n * @returns `undefined`\n * @private\n */\n var _reposition = function() {\n var htmlBridge;\n if (_currentElement && (htmlBridge = _getHtmlBridge(_flashState.bridge))) {\n var pos = _getElementPosition(_currentElement);\n _extend(htmlBridge.style, {\n width: pos.width + \"px\",\n height: pos.height + \"px\",\n top: pos.top + \"px\",\n left: pos.left + \"px\",\n zIndex: \"\" + _getSafeZIndex(_globalConfig.zIndex)\n });\n }\n };\n /**\n * Sends a signal to the Flash object to display the hand cursor if `true`.\n *\n * @returns `undefined`\n * @private\n */\n var _setHandCursor = function(enabled) {\n if (_flashState.ready === true) {\n if (_flashState.bridge && typeof _flashState.bridge.setHandCursor === \"function\") {\n _flashState.bridge.setHandCursor(enabled);\n } else {\n _flashState.ready = false;\n }\n }\n };\n /**\n * Get a safe value for `zIndex`\n *\n * @returns an integer, or \"auto\"\n * @private\n */\n var _getSafeZIndex = function(val) {\n if (/^(?:auto|inherit)$/.test(val)) {\n return val;\n }\n var zIndex;\n if (typeof val === \"number\" && !_isNaN(val)) {\n zIndex = val;\n } else if (typeof val === \"string\") {\n zIndex = _getSafeZIndex(_parseInt(val, 10));\n }\n return typeof zIndex === \"number\" ? zIndex : \"auto\";\n };\n /**\n * Attempt to detect if ZeroClipboard is executing inside of a sandboxed iframe.\n * If it is, Flash Player cannot be used, so ZeroClipboard is dead in the water.\n *\n * @see {@link http://lists.w3.org/Archives/Public/public-whatwg-archive/2014Dec/0002.html}\n * @see {@link https://github.com/zeroclipboard/zeroclipboard/issues/511}\n * @see {@link http://zeroclipboard.org/test-iframes.html}\n *\n * @returns `true` (is sandboxed), `false` (is not sandboxed), or `null` (uncertain) \n * @private\n */\n var _detectSandbox = function(doNotReassessFlashSupport) {\n var effectiveScriptOrigin, frame, frameError, previousState = _flashState.sandboxed, isSandboxed = null;\n doNotReassessFlashSupport = doNotReassessFlashSupport === true;\n if (_pageIsFramed === false) {\n isSandboxed = false;\n } else {\n try {\n frame = window.frameElement || null;\n } catch (e) {\n frameError = {\n name: e.name,\n message: e.message\n };\n }\n if (frame && frame.nodeType === 1 && frame.nodeName === \"IFRAME\") {\n try {\n isSandboxed = frame.hasAttribute(\"sandbox\");\n } catch (e) {\n isSandboxed = null;\n }\n } else {\n try {\n effectiveScriptOrigin = document.domain || null;\n } catch (e) {\n effectiveScriptOrigin = null;\n }\n if (effectiveScriptOrigin === null || frameError && frameError.name === \"SecurityError\" && /(^|[\\s\\(\\[@])sandbox(es|ed|ing|[\\s\\.,!\\)\\]@]|$)/.test(frameError.message.toLowerCase())) {\n isSandboxed = true;\n }\n }\n }\n _flashState.sandboxed = isSandboxed;\n if (previousState !== isSandboxed && !doNotReassessFlashSupport) {\n _detectFlashSupport(_ActiveXObject);\n }\n return isSandboxed;\n };\n /**\n * Detect the Flash Player status, version, and plugin type.\n *\n * @see {@link https://code.google.com/p/doctype-mirror/wiki/ArticleDetectFlash#The_code}\n * @see {@link http://stackoverflow.com/questions/12866060/detecting-pepper-ppapi-flash-with-javascript}\n *\n * @returns `undefined`\n * @private\n */\n var _detectFlashSupport = function(ActiveXObject) {\n var plugin, ax, mimeType, hasFlash = false, isActiveX = false, isPPAPI = false, flashVersion = \"\";\n /**\n * Derived from Apple's suggested sniffer.\n * @param {String} desc e.g. \"Shockwave Flash 7.0 r61\"\n * @returns {String} \"7.0.61\"\n * @private\n */\n function parseFlashVersion(desc) {\n var matches = desc.match(/[\\d]+/g);\n matches.length = 3;\n return matches.join(\".\");\n }\n function isPepperFlash(flashPlayerFileName) {\n return !!flashPlayerFileName && (flashPlayerFileName = flashPlayerFileName.toLowerCase()) && (/^(pepflashplayer\\.dll|libpepflashplayer\\.so|pepperflashplayer\\.plugin)$/.test(flashPlayerFileName) || flashPlayerFileName.slice(-13) === \"chrome.plugin\");\n }\n function inspectPlugin(plugin) {\n if (plugin) {\n hasFlash = true;\n if (plugin.version) {\n flashVersion = parseFlashVersion(plugin.version);\n }\n if (!flashVersion && plugin.description) {\n flashVersion = parseFlashVersion(plugin.description);\n }\n if (plugin.filename) {\n isPPAPI = isPepperFlash(plugin.filename);\n }\n }\n }\n if (_navigator.plugins && _navigator.plugins.length) {\n plugin = _navigator.plugins[\"Shockwave Flash\"];\n inspectPlugin(plugin);\n if (_navigator.plugins[\"Shockwave Flash 2.0\"]) {\n hasFlash = true;\n flashVersion = \"2.0.0.11\";\n }\n } else if (_navigator.mimeTypes && _navigator.mimeTypes.length) {\n mimeType = _navigator.mimeTypes[\"application/x-shockwave-flash\"];\n plugin = mimeType && mimeType.enabledPlugin;\n inspectPlugin(plugin);\n } else if (typeof ActiveXObject !== \"undefined\") {\n isActiveX = true;\n try {\n ax = new ActiveXObject(\"ShockwaveFlash.ShockwaveFlash.7\");\n hasFlash = true;\n flashVersion = parseFlashVersion(ax.GetVariable(\"$version\"));\n } catch (e1) {\n try {\n ax = new ActiveXObject(\"ShockwaveFlash.ShockwaveFlash.6\");\n hasFlash = true;\n flashVersion = \"6.0.21\";\n } catch (e2) {\n try {\n ax = new ActiveXObject(\"ShockwaveFlash.ShockwaveFlash\");\n hasFlash = true;\n flashVersion = parseFlashVersion(ax.GetVariable(\"$version\"));\n } catch (e3) {\n isActiveX = false;\n }\n }\n }\n }\n _flashState.disabled = hasFlash !== true;\n _flashState.outdated = flashVersion && _parseFloat(flashVersion) < _parseFloat(_minimumFlashVersion);\n _flashState.version = flashVersion || \"0.0.0\";\n _flashState.pluginType = isPPAPI ? \"pepper\" : isActiveX ? \"activex\" : hasFlash ? \"netscape\" : \"unknown\";\n };\n /**\n * Invoke the Flash detection algorithms immediately upon inclusion so we're not waiting later.\n */\n _detectFlashSupport(_ActiveXObject);\n /**\n * Always assess the `sandboxed` state of the page at important Flash-related moments.\n */\n _detectSandbox(true);\n /**\n * A shell constructor for `ZeroClipboard` client instances.\n *\n * @constructor\n */\n var ZeroClipboard = function() {\n if (!(this instanceof ZeroClipboard)) {\n return new ZeroClipboard();\n }\n if (typeof ZeroClipboard._createClient === \"function\") {\n ZeroClipboard._createClient.apply(this, _args(arguments));\n }\n };\n /**\n * The ZeroClipboard library's version number.\n *\n * @static\n * @readonly\n * @property {string}\n */\n _defineProperty(ZeroClipboard, \"version\", {\n value: \"2.2.0\",\n writable: false,\n configurable: true,\n enumerable: true\n });\n /**\n * Update or get a copy of the ZeroClipboard global configuration.\n * Returns a copy of the current/updated configuration.\n *\n * @returns Object\n * @static\n */\n ZeroClipboard.config = function() {\n return _config.apply(this, _args(arguments));\n };\n /**\n * Diagnostic method that describes the state of the browser, Flash Player, and ZeroClipboard.\n *\n * @returns Object\n * @static\n */\n ZeroClipboard.state = function() {\n return _state.apply(this, _args(arguments));\n };\n /**\n * Check if Flash is unusable for any reason: disabled, outdated, deactivated, etc.\n *\n * @returns Boolean\n * @static\n */\n ZeroClipboard.isFlashUnusable = function() {\n return _isFlashUnusable.apply(this, _args(arguments));\n };\n /**\n * Register an event listener.\n *\n * @returns `ZeroClipboard`\n * @static\n */\n ZeroClipboard.on = function() {\n return _on.apply(this, _args(arguments));\n };\n /**\n * Unregister an event listener.\n * If no `listener` function/object is provided, it will unregister all listeners for the provided `eventType`.\n * If no `eventType` is provided, it will unregister all listeners for every event type.\n *\n * @returns `ZeroClipboard`\n * @static\n */\n ZeroClipboard.off = function() {\n return _off.apply(this, _args(arguments));\n };\n /**\n * Retrieve event listeners for an `eventType`.\n * If no `eventType` is provided, it will retrieve all listeners for every event type.\n *\n * @returns array of listeners for the `eventType`; if no `eventType`, then a map/hash object of listeners for all event types; or `null`\n */\n ZeroClipboard.handlers = function() {\n return _listeners.apply(this, _args(arguments));\n };\n /**\n * Event emission receiver from the Flash object, forwarding to any registered JavaScript event listeners.\n *\n * @returns For the \"copy\" event, returns the Flash-friendly \"clipData\" object; otherwise `undefined`.\n * @static\n */\n ZeroClipboard.emit = function() {\n return _emit.apply(this, _args(arguments));\n };\n /**\n * Create and embed the Flash object.\n *\n * @returns The Flash object\n * @static\n */\n ZeroClipboard.create = function() {\n return _create.apply(this, _args(arguments));\n };\n /**\n * Self-destruct and clean up everything, including the embedded Flash object.\n *\n * @returns `undefined`\n * @static\n */\n ZeroClipboard.destroy = function() {\n return _destroy.apply(this, _args(arguments));\n };\n /**\n * Set the pending data for clipboard injection.\n *\n * @returns `undefined`\n * @static\n */\n ZeroClipboard.setData = function() {\n return _setData.apply(this, _args(arguments));\n };\n /**\n * Clear the pending data for clipboard injection.\n * If no `format` is provided, all pending data formats will be cleared.\n *\n * @returns `undefined`\n * @static\n */\n ZeroClipboard.clearData = function() {\n return _clearData.apply(this, _args(arguments));\n };\n /**\n * Get a copy of the pending data for clipboard injection.\n * If no `format` is provided, a copy of ALL pending data formats will be returned.\n *\n * @returns `String` or `Object`\n * @static\n */\n ZeroClipboard.getData = function() {\n return _getData.apply(this, _args(arguments));\n };\n /**\n * Sets the current HTML object that the Flash object should overlay. This will put the global\n * Flash object on top of the current element; depending on the setup, this may also set the\n * pending clipboard text data as well as the Flash object's wrapping element's title attribute\n * based on the underlying HTML element and ZeroClipboard configuration.\n *\n * @returns `undefined`\n * @static\n */\n ZeroClipboard.focus = ZeroClipboard.activate = function() {\n return _focus.apply(this, _args(arguments));\n };\n /**\n * Un-overlays the Flash object. This will put the global Flash object off-screen; depending on\n * the setup, this may also unset the Flash object's wrapping element's title attribute based on\n * the underlying HTML element and ZeroClipboard configuration.\n *\n * @returns `undefined`\n * @static\n */\n ZeroClipboard.blur = ZeroClipboard.deactivate = function() {\n return _blur.apply(this, _args(arguments));\n };\n /**\n * Returns the currently focused/\"activated\" HTML element that the Flash object is wrapping.\n *\n * @returns `HTMLElement` or `null`\n * @static\n */\n ZeroClipboard.activeElement = function() {\n return _activeElement.apply(this, _args(arguments));\n };\n if (typeof define === \"function\" && define.amd) {\n define(function() {\n return ZeroClipboard;\n });\n } else if (typeof module === \"object\" && module && typeof module.exports === \"object\" && module.exports) {\n module.exports = ZeroClipboard;\n } else {\n window.ZeroClipboard = ZeroClipboard;\n }\n})(function() {\n return this || window;\n}());"]} \ No newline at end of file diff --git a/bower_components/zeroclipboard/dist/ZeroClipboard.js b/bower_components/zeroclipboard/dist/ZeroClipboard.js new file mode 100644 index 0000000..6e20b59 --- /dev/null +++ b/bower_components/zeroclipboard/dist/ZeroClipboard.js @@ -0,0 +1,2581 @@ +/*! + * ZeroClipboard + * The ZeroClipboard library provides an easy way to copy text to the clipboard using an invisible Adobe Flash movie and a JavaScript interface. + * Copyright (c) 2009-2014 Jon Rohan, James M. Greene + * Licensed MIT + * http://zeroclipboard.org/ + * v2.2.0 + */ +(function(window, undefined) { + "use strict"; + /** + * Store references to critically important global functions that may be + * overridden on certain web pages. + */ + var _window = window, _document = _window.document, _navigator = _window.navigator, _setTimeout = _window.setTimeout, _clearTimeout = _window.clearTimeout, _setInterval = _window.setInterval, _clearInterval = _window.clearInterval, _getComputedStyle = _window.getComputedStyle, _encodeURIComponent = _window.encodeURIComponent, _ActiveXObject = _window.ActiveXObject, _Error = _window.Error, _parseInt = _window.Number.parseInt || _window.parseInt, _parseFloat = _window.Number.parseFloat || _window.parseFloat, _isNaN = _window.Number.isNaN || _window.isNaN, _now = _window.Date.now, _keys = _window.Object.keys, _defineProperty = _window.Object.defineProperty, _hasOwn = _window.Object.prototype.hasOwnProperty, _slice = _window.Array.prototype.slice, _unwrap = function() { + var unwrapper = function(el) { + return el; + }; + if (typeof _window.wrap === "function" && typeof _window.unwrap === "function") { + try { + var div = _document.createElement("div"); + var unwrappedDiv = _window.unwrap(div); + if (div.nodeType === 1 && unwrappedDiv && unwrappedDiv.nodeType === 1) { + unwrapper = _window.unwrap; + } + } catch (e) {} + } + return unwrapper; + }(); + /** + * Convert an `arguments` object into an Array. + * + * @returns The arguments as an Array + * @private + */ + var _args = function(argumentsObj) { + return _slice.call(argumentsObj, 0); + }; + /** + * Shallow-copy the owned, enumerable properties of one object over to another, similar to jQuery's `$.extend`. + * + * @returns The target object, augmented + * @private + */ + var _extend = function() { + var i, len, arg, prop, src, copy, args = _args(arguments), target = args[0] || {}; + for (i = 1, len = args.length; i < len; i++) { + if ((arg = args[i]) != null) { + for (prop in arg) { + if (_hasOwn.call(arg, prop)) { + src = target[prop]; + copy = arg[prop]; + if (target !== copy && copy !== undefined) { + target[prop] = copy; + } + } + } + } + } + return target; + }; + /** + * Return a deep copy of the source object or array. + * + * @returns Object or Array + * @private + */ + var _deepCopy = function(source) { + var copy, i, len, prop; + if (typeof source !== "object" || source == null || typeof source.nodeType === "number") { + copy = source; + } else if (typeof source.length === "number") { + copy = []; + for (i = 0, len = source.length; i < len; i++) { + if (_hasOwn.call(source, i)) { + copy[i] = _deepCopy(source[i]); + } + } + } else { + copy = {}; + for (prop in source) { + if (_hasOwn.call(source, prop)) { + copy[prop] = _deepCopy(source[prop]); + } + } + } + return copy; + }; + /** + * Makes a shallow copy of `obj` (like `_extend`) but filters its properties based on a list of `keys` to keep. + * The inverse of `_omit`, mostly. The big difference is that these properties do NOT need to be enumerable to + * be kept. + * + * @returns A new filtered object. + * @private + */ + var _pick = function(obj, keys) { + var newObj = {}; + for (var i = 0, len = keys.length; i < len; i++) { + if (keys[i] in obj) { + newObj[keys[i]] = obj[keys[i]]; + } + } + return newObj; + }; + /** + * Makes a shallow copy of `obj` (like `_extend`) but filters its properties based on a list of `keys` to omit. + * The inverse of `_pick`. + * + * @returns A new filtered object. + * @private + */ + var _omit = function(obj, keys) { + var newObj = {}; + for (var prop in obj) { + if (keys.indexOf(prop) === -1) { + newObj[prop] = obj[prop]; + } + } + return newObj; + }; + /** + * Remove all owned, enumerable properties from an object. + * + * @returns The original object without its owned, enumerable properties. + * @private + */ + var _deleteOwnProperties = function(obj) { + if (obj) { + for (var prop in obj) { + if (_hasOwn.call(obj, prop)) { + delete obj[prop]; + } + } + } + return obj; + }; + /** + * Determine if an element is contained within another element. + * + * @returns Boolean + * @private + */ + var _containedBy = function(el, ancestorEl) { + if (el && el.nodeType === 1 && el.ownerDocument && ancestorEl && (ancestorEl.nodeType === 1 && ancestorEl.ownerDocument && ancestorEl.ownerDocument === el.ownerDocument || ancestorEl.nodeType === 9 && !ancestorEl.ownerDocument && ancestorEl === el.ownerDocument)) { + do { + if (el === ancestorEl) { + return true; + } + el = el.parentNode; + } while (el); + } + return false; + }; + /** + * Get the URL path's parent directory. + * + * @returns String or `undefined` + * @private + */ + var _getDirPathOfUrl = function(url) { + var dir; + if (typeof url === "string" && url) { + dir = url.split("#")[0].split("?")[0]; + dir = url.slice(0, url.lastIndexOf("/") + 1); + } + return dir; + }; + /** + * Get the current script's URL by throwing an `Error` and analyzing it. + * + * @returns String or `undefined` + * @private + */ + var _getCurrentScriptUrlFromErrorStack = function(stack) { + var url, matches; + if (typeof stack === "string" && stack) { + matches = stack.match(/^(?:|[^:@]*@|.+\)@(?=http[s]?|file)|.+?\s+(?: at |@)(?:[^:\(]+ )*[\(]?)((?:http[s]?|file):\/\/[\/]?.+?\/[^:\)]*?)(?::\d+)(?::\d+)?/); + if (matches && matches[1]) { + url = matches[1]; + } else { + matches = stack.match(/\)@((?:http[s]?|file):\/\/[\/]?.+?\/[^:\)]*?)(?::\d+)(?::\d+)?/); + if (matches && matches[1]) { + url = matches[1]; + } + } + } + return url; + }; + /** + * Get the current script's URL by throwing an `Error` and analyzing it. + * + * @returns String or `undefined` + * @private + */ + var _getCurrentScriptUrlFromError = function() { + var url, err; + try { + throw new _Error(); + } catch (e) { + err = e; + } + if (err) { + url = err.sourceURL || err.fileName || _getCurrentScriptUrlFromErrorStack(err.stack); + } + return url; + }; + /** + * Get the current script's URL. + * + * @returns String or `undefined` + * @private + */ + var _getCurrentScriptUrl = function() { + var jsPath, scripts, i; + if (_document.currentScript && (jsPath = _document.currentScript.src)) { + return jsPath; + } + scripts = _document.getElementsByTagName("script"); + if (scripts.length === 1) { + return scripts[0].src || undefined; + } + if ("readyState" in scripts[0]) { + for (i = scripts.length; i--; ) { + if (scripts[i].readyState === "interactive" && (jsPath = scripts[i].src)) { + return jsPath; + } + } + } + if (_document.readyState === "loading" && (jsPath = scripts[scripts.length - 1].src)) { + return jsPath; + } + if (jsPath = _getCurrentScriptUrlFromError()) { + return jsPath; + } + return undefined; + }; + /** + * Get the unanimous parent directory of ALL script tags. + * If any script tags are either (a) inline or (b) from differing parent + * directories, this method must return `undefined`. + * + * @returns String or `undefined` + * @private + */ + var _getUnanimousScriptParentDir = function() { + var i, jsDir, jsPath, scripts = _document.getElementsByTagName("script"); + for (i = scripts.length; i--; ) { + if (!(jsPath = scripts[i].src)) { + jsDir = null; + break; + } + jsPath = _getDirPathOfUrl(jsPath); + if (jsDir == null) { + jsDir = jsPath; + } else if (jsDir !== jsPath) { + jsDir = null; + break; + } + } + return jsDir || undefined; + }; + /** + * Get the presumed location of the "ZeroClipboard.swf" file, based on the location + * of the executing JavaScript file (e.g. "ZeroClipboard.js", etc.). + * + * @returns String + * @private + */ + var _getDefaultSwfPath = function() { + var jsDir = _getDirPathOfUrl(_getCurrentScriptUrl()) || _getUnanimousScriptParentDir() || ""; + return jsDir + "ZeroClipboard.swf"; + }; + /** + * Keep track of if the page is framed (in an `iframe`). This can never change. + * @private + */ + var _pageIsFramed = function() { + return window.opener == null && (!!window.top && window != window.top || !!window.parent && window != window.parent); + }(); + /** + * Keep track of the state of the Flash object. + * @private + */ + var _flashState = { + bridge: null, + version: "0.0.0", + pluginType: "unknown", + disabled: null, + outdated: null, + sandboxed: null, + unavailable: null, + degraded: null, + deactivated: null, + overdue: null, + ready: null + }; + /** + * The minimum Flash Player version required to use ZeroClipboard completely. + * @readonly + * @private + */ + var _minimumFlashVersion = "11.0.0"; + /** + * The ZeroClipboard library version number, as reported by Flash, at the time the SWF was compiled. + */ + var _zcSwfVersion; + /** + * Keep track of all event listener registrations. + * @private + */ + var _handlers = {}; + /** + * Keep track of the currently activated element. + * @private + */ + var _currentElement; + /** + * Keep track of the element that was activated when a `copy` process started. + * @private + */ + var _copyTarget; + /** + * Keep track of data for the pending clipboard transaction. + * @private + */ + var _clipData = {}; + /** + * Keep track of data formats for the pending clipboard transaction. + * @private + */ + var _clipDataFormatMap = null; + /** + * Keep track of the Flash availability check timeout. + * @private + */ + var _flashCheckTimeout = 0; + /** + * Keep track of SWF network errors interval polling. + * @private + */ + var _swfFallbackCheckInterval = 0; + /** + * The `message` store for events + * @private + */ + var _eventMessages = { + ready: "Flash communication is established", + error: { + "flash-disabled": "Flash is disabled or not installed. May also be attempting to run Flash in a sandboxed iframe, which is impossible.", + "flash-outdated": "Flash is too outdated to support ZeroClipboard", + "flash-sandboxed": "Attempting to run Flash in a sandboxed iframe, which is impossible", + "flash-unavailable": "Flash is unable to communicate bidirectionally with JavaScript", + "flash-degraded": "Flash is unable to preserve data fidelity when communicating with JavaScript", + "flash-deactivated": "Flash is too outdated for your browser and/or is configured as click-to-activate.\nThis may also mean that the ZeroClipboard SWF object could not be loaded, so please check your `swfPath` configuration and/or network connectivity.\nMay also be attempting to run Flash in a sandboxed iframe, which is impossible.", + "flash-overdue": "Flash communication was established but NOT within the acceptable time limit", + "version-mismatch": "ZeroClipboard JS version number does not match ZeroClipboard SWF version number", + "clipboard-error": "At least one error was thrown while ZeroClipboard was attempting to inject your data into the clipboard", + "config-mismatch": "ZeroClipboard configuration does not match Flash's reality", + "swf-not-found": "The ZeroClipboard SWF object could not be loaded, so please check your `swfPath` configuration and/or network connectivity" + } + }; + /** + * The `name`s of `error` events that can only occur is Flash has at least + * been able to load the SWF successfully. + * @private + */ + var _errorsThatOnlyOccurAfterFlashLoads = [ "flash-unavailable", "flash-degraded", "flash-overdue", "version-mismatch", "config-mismatch", "clipboard-error" ]; + /** + * The `name`s of `error` events that should likely result in the `_flashState` + * variable's property values being updated. + * @private + */ + var _flashStateErrorNames = [ "flash-disabled", "flash-outdated", "flash-sandboxed", "flash-unavailable", "flash-degraded", "flash-deactivated", "flash-overdue" ]; + /** + * A RegExp to match the `name` property of `error` events related to Flash. + * @private + */ + var _flashStateErrorNameMatchingRegex = new RegExp("^flash-(" + _flashStateErrorNames.map(function(errorName) { + return errorName.replace(/^flash-/, ""); + }).join("|") + ")$"); + /** + * A RegExp to match the `name` property of `error` events related to Flash, + * which is enabled. + * @private + */ + var _flashStateEnabledErrorNameMatchingRegex = new RegExp("^flash-(" + _flashStateErrorNames.slice(1).map(function(errorName) { + return errorName.replace(/^flash-/, ""); + }).join("|") + ")$"); + /** + * ZeroClipboard configuration defaults for the Core module. + * @private + */ + var _globalConfig = { + swfPath: _getDefaultSwfPath(), + trustedDomains: window.location.host ? [ window.location.host ] : [], + cacheBust: true, + forceEnhancedClipboard: false, + flashLoadTimeout: 3e4, + autoActivate: true, + bubbleEvents: true, + containerId: "global-zeroclipboard-html-bridge", + containerClass: "global-zeroclipboard-container", + swfObjectId: "global-zeroclipboard-flash-bridge", + hoverClass: "zeroclipboard-is-hover", + activeClass: "zeroclipboard-is-active", + forceHandCursor: false, + title: null, + zIndex: 999999999 + }; + /** + * The underlying implementation of `ZeroClipboard.config`. + * @private + */ + var _config = function(options) { + if (typeof options === "object" && options !== null) { + for (var prop in options) { + if (_hasOwn.call(options, prop)) { + if (/^(?:forceHandCursor|title|zIndex|bubbleEvents)$/.test(prop)) { + _globalConfig[prop] = options[prop]; + } else if (_flashState.bridge == null) { + if (prop === "containerId" || prop === "swfObjectId") { + if (_isValidHtml4Id(options[prop])) { + _globalConfig[prop] = options[prop]; + } else { + throw new Error("The specified `" + prop + "` value is not valid as an HTML4 Element ID"); + } + } else { + _globalConfig[prop] = options[prop]; + } + } + } + } + } + if (typeof options === "string" && options) { + if (_hasOwn.call(_globalConfig, options)) { + return _globalConfig[options]; + } + return; + } + return _deepCopy(_globalConfig); + }; + /** + * The underlying implementation of `ZeroClipboard.state`. + * @private + */ + var _state = function() { + _detectSandbox(); + return { + browser: _pick(_navigator, [ "userAgent", "platform", "appName" ]), + flash: _omit(_flashState, [ "bridge" ]), + zeroclipboard: { + version: ZeroClipboard.version, + config: ZeroClipboard.config() + } + }; + }; + /** + * The underlying implementation of `ZeroClipboard.isFlashUnusable`. + * @private + */ + var _isFlashUnusable = function() { + return !!(_flashState.disabled || _flashState.outdated || _flashState.sandboxed || _flashState.unavailable || _flashState.degraded || _flashState.deactivated); + }; + /** + * The underlying implementation of `ZeroClipboard.on`. + * @private + */ + var _on = function(eventType, listener) { + var i, len, events, added = {}; + if (typeof eventType === "string" && eventType) { + events = eventType.toLowerCase().split(/\s+/); + } else if (typeof eventType === "object" && eventType && typeof listener === "undefined") { + for (i in eventType) { + if (_hasOwn.call(eventType, i) && typeof i === "string" && i && typeof eventType[i] === "function") { + ZeroClipboard.on(i, eventType[i]); + } + } + } + if (events && events.length) { + for (i = 0, len = events.length; i < len; i++) { + eventType = events[i].replace(/^on/, ""); + added[eventType] = true; + if (!_handlers[eventType]) { + _handlers[eventType] = []; + } + _handlers[eventType].push(listener); + } + if (added.ready && _flashState.ready) { + ZeroClipboard.emit({ + type: "ready" + }); + } + if (added.error) { + for (i = 0, len = _flashStateErrorNames.length; i < len; i++) { + if (_flashState[_flashStateErrorNames[i].replace(/^flash-/, "")] === true) { + ZeroClipboard.emit({ + type: "error", + name: _flashStateErrorNames[i] + }); + break; + } + } + if (_zcSwfVersion !== undefined && ZeroClipboard.version !== _zcSwfVersion) { + ZeroClipboard.emit({ + type: "error", + name: "version-mismatch", + jsVersion: ZeroClipboard.version, + swfVersion: _zcSwfVersion + }); + } + } + } + return ZeroClipboard; + }; + /** + * The underlying implementation of `ZeroClipboard.off`. + * @private + */ + var _off = function(eventType, listener) { + var i, len, foundIndex, events, perEventHandlers; + if (arguments.length === 0) { + events = _keys(_handlers); + } else if (typeof eventType === "string" && eventType) { + events = eventType.split(/\s+/); + } else if (typeof eventType === "object" && eventType && typeof listener === "undefined") { + for (i in eventType) { + if (_hasOwn.call(eventType, i) && typeof i === "string" && i && typeof eventType[i] === "function") { + ZeroClipboard.off(i, eventType[i]); + } + } + } + if (events && events.length) { + for (i = 0, len = events.length; i < len; i++) { + eventType = events[i].toLowerCase().replace(/^on/, ""); + perEventHandlers = _handlers[eventType]; + if (perEventHandlers && perEventHandlers.length) { + if (listener) { + foundIndex = perEventHandlers.indexOf(listener); + while (foundIndex !== -1) { + perEventHandlers.splice(foundIndex, 1); + foundIndex = perEventHandlers.indexOf(listener, foundIndex); + } + } else { + perEventHandlers.length = 0; + } + } + } + } + return ZeroClipboard; + }; + /** + * The underlying implementation of `ZeroClipboard.handlers`. + * @private + */ + var _listeners = function(eventType) { + var copy; + if (typeof eventType === "string" && eventType) { + copy = _deepCopy(_handlers[eventType]) || null; + } else { + copy = _deepCopy(_handlers); + } + return copy; + }; + /** + * The underlying implementation of `ZeroClipboard.emit`. + * @private + */ + var _emit = function(event) { + var eventCopy, returnVal, tmp; + event = _createEvent(event); + if (!event) { + return; + } + if (_preprocessEvent(event)) { + return; + } + if (event.type === "ready" && _flashState.overdue === true) { + return ZeroClipboard.emit({ + type: "error", + name: "flash-overdue" + }); + } + eventCopy = _extend({}, event); + _dispatchCallbacks.call(this, eventCopy); + if (event.type === "copy") { + tmp = _mapClipDataToFlash(_clipData); + returnVal = tmp.data; + _clipDataFormatMap = tmp.formatMap; + } + return returnVal; + }; + /** + * The underlying implementation of `ZeroClipboard.create`. + * @private + */ + var _create = function() { + var previousState = _flashState.sandboxed; + _detectSandbox(); + if (typeof _flashState.ready !== "boolean") { + _flashState.ready = false; + } + if (_flashState.sandboxed !== previousState && _flashState.sandboxed === true) { + _flashState.ready = false; + ZeroClipboard.emit({ + type: "error", + name: "flash-sandboxed" + }); + } else if (!ZeroClipboard.isFlashUnusable() && _flashState.bridge === null) { + var maxWait = _globalConfig.flashLoadTimeout; + if (typeof maxWait === "number" && maxWait >= 0) { + _flashCheckTimeout = _setTimeout(function() { + if (typeof _flashState.deactivated !== "boolean") { + _flashState.deactivated = true; + } + if (_flashState.deactivated === true) { + ZeroClipboard.emit({ + type: "error", + name: "flash-deactivated" + }); + } + }, maxWait); + } + _flashState.overdue = false; + _embedSwf(); + } + }; + /** + * The underlying implementation of `ZeroClipboard.destroy`. + * @private + */ + var _destroy = function() { + ZeroClipboard.clearData(); + ZeroClipboard.blur(); + ZeroClipboard.emit("destroy"); + _unembedSwf(); + ZeroClipboard.off(); + }; + /** + * The underlying implementation of `ZeroClipboard.setData`. + * @private + */ + var _setData = function(format, data) { + var dataObj; + if (typeof format === "object" && format && typeof data === "undefined") { + dataObj = format; + ZeroClipboard.clearData(); + } else if (typeof format === "string" && format) { + dataObj = {}; + dataObj[format] = data; + } else { + return; + } + for (var dataFormat in dataObj) { + if (typeof dataFormat === "string" && dataFormat && _hasOwn.call(dataObj, dataFormat) && typeof dataObj[dataFormat] === "string" && dataObj[dataFormat]) { + _clipData[dataFormat] = dataObj[dataFormat]; + } + } + }; + /** + * The underlying implementation of `ZeroClipboard.clearData`. + * @private + */ + var _clearData = function(format) { + if (typeof format === "undefined") { + _deleteOwnProperties(_clipData); + _clipDataFormatMap = null; + } else if (typeof format === "string" && _hasOwn.call(_clipData, format)) { + delete _clipData[format]; + } + }; + /** + * The underlying implementation of `ZeroClipboard.getData`. + * @private + */ + var _getData = function(format) { + if (typeof format === "undefined") { + return _deepCopy(_clipData); + } else if (typeof format === "string" && _hasOwn.call(_clipData, format)) { + return _clipData[format]; + } + }; + /** + * The underlying implementation of `ZeroClipboard.focus`/`ZeroClipboard.activate`. + * @private + */ + var _focus = function(element) { + if (!(element && element.nodeType === 1)) { + return; + } + if (_currentElement) { + _removeClass(_currentElement, _globalConfig.activeClass); + if (_currentElement !== element) { + _removeClass(_currentElement, _globalConfig.hoverClass); + } + } + _currentElement = element; + _addClass(element, _globalConfig.hoverClass); + var newTitle = element.getAttribute("title") || _globalConfig.title; + if (typeof newTitle === "string" && newTitle) { + var htmlBridge = _getHtmlBridge(_flashState.bridge); + if (htmlBridge) { + htmlBridge.setAttribute("title", newTitle); + } + } + var useHandCursor = _globalConfig.forceHandCursor === true || _getStyle(element, "cursor") === "pointer"; + _setHandCursor(useHandCursor); + _reposition(); + }; + /** + * The underlying implementation of `ZeroClipboard.blur`/`ZeroClipboard.deactivate`. + * @private + */ + var _blur = function() { + var htmlBridge = _getHtmlBridge(_flashState.bridge); + if (htmlBridge) { + htmlBridge.removeAttribute("title"); + htmlBridge.style.left = "0px"; + htmlBridge.style.top = "-9999px"; + htmlBridge.style.width = "1px"; + htmlBridge.style.height = "1px"; + } + if (_currentElement) { + _removeClass(_currentElement, _globalConfig.hoverClass); + _removeClass(_currentElement, _globalConfig.activeClass); + _currentElement = null; + } + }; + /** + * The underlying implementation of `ZeroClipboard.activeElement`. + * @private + */ + var _activeElement = function() { + return _currentElement || null; + }; + /** + * Check if a value is a valid HTML4 `ID` or `Name` token. + * @private + */ + var _isValidHtml4Id = function(id) { + return typeof id === "string" && id && /^[A-Za-z][A-Za-z0-9_:\-\.]*$/.test(id); + }; + /** + * Create or update an `event` object, based on the `eventType`. + * @private + */ + var _createEvent = function(event) { + var eventType; + if (typeof event === "string" && event) { + eventType = event; + event = {}; + } else if (typeof event === "object" && event && typeof event.type === "string" && event.type) { + eventType = event.type; + } + if (!eventType) { + return; + } + eventType = eventType.toLowerCase(); + if (!event.target && (/^(copy|aftercopy|_click)$/.test(eventType) || eventType === "error" && event.name === "clipboard-error")) { + event.target = _copyTarget; + } + _extend(event, { + type: eventType, + target: event.target || _currentElement || null, + relatedTarget: event.relatedTarget || null, + currentTarget: _flashState && _flashState.bridge || null, + timeStamp: event.timeStamp || _now() || null + }); + var msg = _eventMessages[event.type]; + if (event.type === "error" && event.name && msg) { + msg = msg[event.name]; + } + if (msg) { + event.message = msg; + } + if (event.type === "ready") { + _extend(event, { + target: null, + version: _flashState.version + }); + } + if (event.type === "error") { + if (_flashStateErrorNameMatchingRegex.test(event.name)) { + _extend(event, { + target: null, + minimumVersion: _minimumFlashVersion + }); + } + if (_flashStateEnabledErrorNameMatchingRegex.test(event.name)) { + _extend(event, { + version: _flashState.version + }); + } + } + if (event.type === "copy") { + event.clipboardData = { + setData: ZeroClipboard.setData, + clearData: ZeroClipboard.clearData + }; + } + if (event.type === "aftercopy") { + event = _mapClipResultsFromFlash(event, _clipDataFormatMap); + } + if (event.target && !event.relatedTarget) { + event.relatedTarget = _getRelatedTarget(event.target); + } + return _addMouseData(event); + }; + /** + * Get a relatedTarget from the target's `data-clipboard-target` attribute + * @private + */ + var _getRelatedTarget = function(targetEl) { + var relatedTargetId = targetEl && targetEl.getAttribute && targetEl.getAttribute("data-clipboard-target"); + return relatedTargetId ? _document.getElementById(relatedTargetId) : null; + }; + /** + * Add element and position data to `MouseEvent` instances + * @private + */ + var _addMouseData = function(event) { + if (event && /^_(?:click|mouse(?:over|out|down|up|move))$/.test(event.type)) { + var srcElement = event.target; + var fromElement = event.type === "_mouseover" && event.relatedTarget ? event.relatedTarget : undefined; + var toElement = event.type === "_mouseout" && event.relatedTarget ? event.relatedTarget : undefined; + var pos = _getElementPosition(srcElement); + var screenLeft = _window.screenLeft || _window.screenX || 0; + var screenTop = _window.screenTop || _window.screenY || 0; + var scrollLeft = _document.body.scrollLeft + _document.documentElement.scrollLeft; + var scrollTop = _document.body.scrollTop + _document.documentElement.scrollTop; + var pageX = pos.left + (typeof event._stageX === "number" ? event._stageX : 0); + var pageY = pos.top + (typeof event._stageY === "number" ? event._stageY : 0); + var clientX = pageX - scrollLeft; + var clientY = pageY - scrollTop; + var screenX = screenLeft + clientX; + var screenY = screenTop + clientY; + var moveX = typeof event.movementX === "number" ? event.movementX : 0; + var moveY = typeof event.movementY === "number" ? event.movementY : 0; + delete event._stageX; + delete event._stageY; + _extend(event, { + srcElement: srcElement, + fromElement: fromElement, + toElement: toElement, + screenX: screenX, + screenY: screenY, + pageX: pageX, + pageY: pageY, + clientX: clientX, + clientY: clientY, + x: clientX, + y: clientY, + movementX: moveX, + movementY: moveY, + offsetX: 0, + offsetY: 0, + layerX: 0, + layerY: 0 + }); + } + return event; + }; + /** + * Determine if an event's registered handlers should be execute synchronously or asynchronously. + * + * @returns {boolean} + * @private + */ + var _shouldPerformAsync = function(event) { + var eventType = event && typeof event.type === "string" && event.type || ""; + return !/^(?:(?:before)?copy|destroy)$/.test(eventType); + }; + /** + * Control if a callback should be executed asynchronously or not. + * + * @returns `undefined` + * @private + */ + var _dispatchCallback = function(func, context, args, async) { + if (async) { + _setTimeout(function() { + func.apply(context, args); + }, 0); + } else { + func.apply(context, args); + } + }; + /** + * Handle the actual dispatching of events to client instances. + * + * @returns `undefined` + * @private + */ + var _dispatchCallbacks = function(event) { + if (!(typeof event === "object" && event && event.type)) { + return; + } + var async = _shouldPerformAsync(event); + var wildcardTypeHandlers = _handlers["*"] || []; + var specificTypeHandlers = _handlers[event.type] || []; + var handlers = wildcardTypeHandlers.concat(specificTypeHandlers); + if (handlers && handlers.length) { + var i, len, func, context, eventCopy, originalContext = this; + for (i = 0, len = handlers.length; i < len; i++) { + func = handlers[i]; + context = originalContext; + if (typeof func === "string" && typeof _window[func] === "function") { + func = _window[func]; + } + if (typeof func === "object" && func && typeof func.handleEvent === "function") { + context = func; + func = func.handleEvent; + } + if (typeof func === "function") { + eventCopy = _extend({}, event); + _dispatchCallback(func, context, [ eventCopy ], async); + } + } + } + return this; + }; + /** + * Check an `error` event's `name` property to see if Flash has + * already loaded, which rules out possible `iframe` sandboxing. + * @private + */ + var _getSandboxStatusFromErrorEvent = function(event) { + var isSandboxed = null; + if (_pageIsFramed === false || event && event.type === "error" && event.name && _errorsThatOnlyOccurAfterFlashLoads.indexOf(event.name) !== -1) { + isSandboxed = false; + } + return isSandboxed; + }; + /** + * Preprocess any special behaviors, reactions, or state changes after receiving this event. + * Executes only once per event emitted, NOT once per client. + * @private + */ + var _preprocessEvent = function(event) { + var element = event.target || _currentElement || null; + var sourceIsSwf = event._source === "swf"; + delete event._source; + switch (event.type) { + case "error": + var isSandboxed = event.name === "flash-sandboxed" || _getSandboxStatusFromErrorEvent(event); + if (typeof isSandboxed === "boolean") { + _flashState.sandboxed = isSandboxed; + } + if (_flashStateErrorNames.indexOf(event.name) !== -1) { + _extend(_flashState, { + disabled: event.name === "flash-disabled", + outdated: event.name === "flash-outdated", + unavailable: event.name === "flash-unavailable", + degraded: event.name === "flash-degraded", + deactivated: event.name === "flash-deactivated", + overdue: event.name === "flash-overdue", + ready: false + }); + } else if (event.name === "version-mismatch") { + _zcSwfVersion = event.swfVersion; + _extend(_flashState, { + disabled: false, + outdated: false, + unavailable: false, + degraded: false, + deactivated: false, + overdue: false, + ready: false + }); + } + _clearTimeoutsAndPolling(); + break; + + case "ready": + _zcSwfVersion = event.swfVersion; + var wasDeactivated = _flashState.deactivated === true; + _extend(_flashState, { + disabled: false, + outdated: false, + sandboxed: false, + unavailable: false, + degraded: false, + deactivated: false, + overdue: wasDeactivated, + ready: !wasDeactivated + }); + _clearTimeoutsAndPolling(); + break; + + case "beforecopy": + _copyTarget = element; + break; + + case "copy": + var textContent, htmlContent, targetEl = event.relatedTarget; + if (!(_clipData["text/html"] || _clipData["text/plain"]) && targetEl && (htmlContent = targetEl.value || targetEl.outerHTML || targetEl.innerHTML) && (textContent = targetEl.value || targetEl.textContent || targetEl.innerText)) { + event.clipboardData.clearData(); + event.clipboardData.setData("text/plain", textContent); + if (htmlContent !== textContent) { + event.clipboardData.setData("text/html", htmlContent); + } + } else if (!_clipData["text/plain"] && event.target && (textContent = event.target.getAttribute("data-clipboard-text"))) { + event.clipboardData.clearData(); + event.clipboardData.setData("text/plain", textContent); + } + break; + + case "aftercopy": + _queueEmitClipboardErrors(event); + ZeroClipboard.clearData(); + if (element && element !== _safeActiveElement() && element.focus) { + element.focus(); + } + break; + + case "_mouseover": + ZeroClipboard.focus(element); + if (_globalConfig.bubbleEvents === true && sourceIsSwf) { + if (element && element !== event.relatedTarget && !_containedBy(event.relatedTarget, element)) { + _fireMouseEvent(_extend({}, event, { + type: "mouseenter", + bubbles: false, + cancelable: false + })); + } + _fireMouseEvent(_extend({}, event, { + type: "mouseover" + })); + } + break; + + case "_mouseout": + ZeroClipboard.blur(); + if (_globalConfig.bubbleEvents === true && sourceIsSwf) { + if (element && element !== event.relatedTarget && !_containedBy(event.relatedTarget, element)) { + _fireMouseEvent(_extend({}, event, { + type: "mouseleave", + bubbles: false, + cancelable: false + })); + } + _fireMouseEvent(_extend({}, event, { + type: "mouseout" + })); + } + break; + + case "_mousedown": + _addClass(element, _globalConfig.activeClass); + if (_globalConfig.bubbleEvents === true && sourceIsSwf) { + _fireMouseEvent(_extend({}, event, { + type: event.type.slice(1) + })); + } + break; + + case "_mouseup": + _removeClass(element, _globalConfig.activeClass); + if (_globalConfig.bubbleEvents === true && sourceIsSwf) { + _fireMouseEvent(_extend({}, event, { + type: event.type.slice(1) + })); + } + break; + + case "_click": + _copyTarget = null; + if (_globalConfig.bubbleEvents === true && sourceIsSwf) { + _fireMouseEvent(_extend({}, event, { + type: event.type.slice(1) + })); + } + break; + + case "_mousemove": + if (_globalConfig.bubbleEvents === true && sourceIsSwf) { + _fireMouseEvent(_extend({}, event, { + type: event.type.slice(1) + })); + } + break; + } + if (/^_(?:click|mouse(?:over|out|down|up|move))$/.test(event.type)) { + return true; + } + }; + /** + * Check an "aftercopy" event for clipboard errors and emit a corresponding "error" event. + * @private + */ + var _queueEmitClipboardErrors = function(aftercopyEvent) { + if (aftercopyEvent.errors && aftercopyEvent.errors.length > 0) { + var errorEvent = _deepCopy(aftercopyEvent); + _extend(errorEvent, { + type: "error", + name: "clipboard-error" + }); + delete errorEvent.success; + _setTimeout(function() { + ZeroClipboard.emit(errorEvent); + }, 0); + } + }; + /** + * Dispatch a synthetic MouseEvent. + * + * @returns `undefined` + * @private + */ + var _fireMouseEvent = function(event) { + if (!(event && typeof event.type === "string" && event)) { + return; + } + var e, target = event.target || null, doc = target && target.ownerDocument || _document, defaults = { + view: doc.defaultView || _window, + canBubble: true, + cancelable: true, + detail: event.type === "click" ? 1 : 0, + button: typeof event.which === "number" ? event.which - 1 : typeof event.button === "number" ? event.button : doc.createEvent ? 0 : 1 + }, args = _extend(defaults, event); + if (!target) { + return; + } + if (doc.createEvent && target.dispatchEvent) { + args = [ args.type, args.canBubble, args.cancelable, args.view, args.detail, args.screenX, args.screenY, args.clientX, args.clientY, args.ctrlKey, args.altKey, args.shiftKey, args.metaKey, args.button, args.relatedTarget ]; + e = doc.createEvent("MouseEvents"); + if (e.initMouseEvent) { + e.initMouseEvent.apply(e, args); + e._source = "js"; + target.dispatchEvent(e); + } + } + }; + /** + * Continuously poll the DOM until either: + * (a) the fallback content becomes visible, or + * (b) we receive an event from SWF (handled elsewhere) + * + * IMPORTANT: + * This is NOT a necessary check but it can result in significantly faster + * detection of bad `swfPath` configuration and/or network/server issues [in + * supported browsers] than waiting for the entire `flashLoadTimeout` duration + * to elapse before detecting that the SWF cannot be loaded. The detection + * duration can be anywhere from 10-30 times faster [in supported browsers] by + * using this approach. + * + * @returns `undefined` + * @private + */ + var _watchForSwfFallbackContent = function() { + var maxWait = _globalConfig.flashLoadTimeout; + if (typeof maxWait === "number" && maxWait >= 0) { + var pollWait = Math.min(1e3, maxWait / 10); + var fallbackContentId = _globalConfig.swfObjectId + "_fallbackContent"; + _swfFallbackCheckInterval = _setInterval(function() { + var el = _document.getElementById(fallbackContentId); + if (_isElementVisible(el)) { + _clearTimeoutsAndPolling(); + _flashState.deactivated = null; + ZeroClipboard.emit({ + type: "error", + name: "swf-not-found" + }); + } + }, pollWait); + } + }; + /** + * Create the HTML bridge element to embed the Flash object into. + * @private + */ + var _createHtmlBridge = function() { + var container = _document.createElement("div"); + container.id = _globalConfig.containerId; + container.className = _globalConfig.containerClass; + container.style.position = "absolute"; + container.style.left = "0px"; + container.style.top = "-9999px"; + container.style.width = "1px"; + container.style.height = "1px"; + container.style.zIndex = "" + _getSafeZIndex(_globalConfig.zIndex); + return container; + }; + /** + * Get the HTML element container that wraps the Flash bridge object/element. + * @private + */ + var _getHtmlBridge = function(flashBridge) { + var htmlBridge = flashBridge && flashBridge.parentNode; + while (htmlBridge && htmlBridge.nodeName === "OBJECT" && htmlBridge.parentNode) { + htmlBridge = htmlBridge.parentNode; + } + return htmlBridge || null; + }; + /** + * Create the SWF object. + * + * @returns The SWF object reference. + * @private + */ + var _embedSwf = function() { + var len, flashBridge = _flashState.bridge, container = _getHtmlBridge(flashBridge); + if (!flashBridge) { + var allowScriptAccess = _determineScriptAccess(_window.location.host, _globalConfig); + var allowNetworking = allowScriptAccess === "never" ? "none" : "all"; + var flashvars = _vars(_extend({ + jsVersion: ZeroClipboard.version + }, _globalConfig)); + var swfUrl = _globalConfig.swfPath + _cacheBust(_globalConfig.swfPath, _globalConfig); + container = _createHtmlBridge(); + var divToBeReplaced = _document.createElement("div"); + container.appendChild(divToBeReplaced); + _document.body.appendChild(container); + var tmpDiv = _document.createElement("div"); + var usingActiveX = _flashState.pluginType === "activex"; + tmpDiv.innerHTML = '" + (usingActiveX ? '' : "") + '' + '' + '' + '' + '' + '
 
' + "
"; + flashBridge = tmpDiv.firstChild; + tmpDiv = null; + _unwrap(flashBridge).ZeroClipboard = ZeroClipboard; + container.replaceChild(flashBridge, divToBeReplaced); + _watchForSwfFallbackContent(); + } + if (!flashBridge) { + flashBridge = _document[_globalConfig.swfObjectId]; + if (flashBridge && (len = flashBridge.length)) { + flashBridge = flashBridge[len - 1]; + } + if (!flashBridge && container) { + flashBridge = container.firstChild; + } + } + _flashState.bridge = flashBridge || null; + return flashBridge; + }; + /** + * Destroy the SWF object. + * @private + */ + var _unembedSwf = function() { + var flashBridge = _flashState.bridge; + if (flashBridge) { + var htmlBridge = _getHtmlBridge(flashBridge); + if (htmlBridge) { + if (_flashState.pluginType === "activex" && "readyState" in flashBridge) { + flashBridge.style.display = "none"; + (function removeSwfFromIE() { + if (flashBridge.readyState === 4) { + for (var prop in flashBridge) { + if (typeof flashBridge[prop] === "function") { + flashBridge[prop] = null; + } + } + if (flashBridge.parentNode) { + flashBridge.parentNode.removeChild(flashBridge); + } + if (htmlBridge.parentNode) { + htmlBridge.parentNode.removeChild(htmlBridge); + } + } else { + _setTimeout(removeSwfFromIE, 10); + } + })(); + } else { + if (flashBridge.parentNode) { + flashBridge.parentNode.removeChild(flashBridge); + } + if (htmlBridge.parentNode) { + htmlBridge.parentNode.removeChild(htmlBridge); + } + } + } + _clearTimeoutsAndPolling(); + _flashState.ready = null; + _flashState.bridge = null; + _flashState.deactivated = null; + _zcSwfVersion = undefined; + } + }; + /** + * Map the data format names of the "clipData" to Flash-friendly names. + * + * @returns A new transformed object. + * @private + */ + var _mapClipDataToFlash = function(clipData) { + var newClipData = {}, formatMap = {}; + if (!(typeof clipData === "object" && clipData)) { + return; + } + for (var dataFormat in clipData) { + if (dataFormat && _hasOwn.call(clipData, dataFormat) && typeof clipData[dataFormat] === "string" && clipData[dataFormat]) { + switch (dataFormat.toLowerCase()) { + case "text/plain": + case "text": + case "air:text": + case "flash:text": + newClipData.text = clipData[dataFormat]; + formatMap.text = dataFormat; + break; + + case "text/html": + case "html": + case "air:html": + case "flash:html": + newClipData.html = clipData[dataFormat]; + formatMap.html = dataFormat; + break; + + case "application/rtf": + case "text/rtf": + case "rtf": + case "richtext": + case "air:rtf": + case "flash:rtf": + newClipData.rtf = clipData[dataFormat]; + formatMap.rtf = dataFormat; + break; + + default: + break; + } + } + } + return { + data: newClipData, + formatMap: formatMap + }; + }; + /** + * Map the data format names from Flash-friendly names back to their original "clipData" names (via a format mapping). + * + * @returns A new transformed object. + * @private + */ + var _mapClipResultsFromFlash = function(clipResults, formatMap) { + if (!(typeof clipResults === "object" && clipResults && typeof formatMap === "object" && formatMap)) { + return clipResults; + } + var newResults = {}; + for (var prop in clipResults) { + if (_hasOwn.call(clipResults, prop)) { + if (prop === "errors") { + newResults[prop] = clipResults[prop] ? clipResults[prop].slice() : []; + for (var i = 0, len = newResults[prop].length; i < len; i++) { + newResults[prop][i].format = formatMap[newResults[prop][i].format]; + } + } else if (prop !== "success" && prop !== "data") { + newResults[prop] = clipResults[prop]; + } else { + newResults[prop] = {}; + var tmpHash = clipResults[prop]; + for (var dataFormat in tmpHash) { + if (dataFormat && _hasOwn.call(tmpHash, dataFormat) && _hasOwn.call(formatMap, dataFormat)) { + newResults[prop][formatMap[dataFormat]] = tmpHash[dataFormat]; + } + } + } + } + } + return newResults; + }; + /** + * Will look at a path, and will create a "?noCache={time}" or "&noCache={time}" + * query param string to return. Does NOT append that string to the original path. + * This is useful because ExternalInterface often breaks when a Flash SWF is cached. + * + * @returns The `noCache` query param with necessary "?"/"&" prefix. + * @private + */ + var _cacheBust = function(path, options) { + var cacheBust = options == null || options && options.cacheBust === true; + if (cacheBust) { + return (path.indexOf("?") === -1 ? "?" : "&") + "noCache=" + _now(); + } else { + return ""; + } + }; + /** + * Creates a query string for the FlashVars param. + * Does NOT include the cache-busting query param. + * + * @returns FlashVars query string + * @private + */ + var _vars = function(options) { + var i, len, domain, domains, str = "", trustedOriginsExpanded = []; + if (options.trustedDomains) { + if (typeof options.trustedDomains === "string") { + domains = [ options.trustedDomains ]; + } else if (typeof options.trustedDomains === "object" && "length" in options.trustedDomains) { + domains = options.trustedDomains; + } + } + if (domains && domains.length) { + for (i = 0, len = domains.length; i < len; i++) { + if (_hasOwn.call(domains, i) && domains[i] && typeof domains[i] === "string") { + domain = _extractDomain(domains[i]); + if (!domain) { + continue; + } + if (domain === "*") { + trustedOriginsExpanded.length = 0; + trustedOriginsExpanded.push(domain); + break; + } + trustedOriginsExpanded.push.apply(trustedOriginsExpanded, [ domain, "//" + domain, _window.location.protocol + "//" + domain ]); + } + } + } + if (trustedOriginsExpanded.length) { + str += "trustedOrigins=" + _encodeURIComponent(trustedOriginsExpanded.join(",")); + } + if (options.forceEnhancedClipboard === true) { + str += (str ? "&" : "") + "forceEnhancedClipboard=true"; + } + if (typeof options.swfObjectId === "string" && options.swfObjectId) { + str += (str ? "&" : "") + "swfObjectId=" + _encodeURIComponent(options.swfObjectId); + } + if (typeof options.jsVersion === "string" && options.jsVersion) { + str += (str ? "&" : "") + "jsVersion=" + _encodeURIComponent(options.jsVersion); + } + return str; + }; + /** + * Extract the domain (e.g. "github.com") from an origin (e.g. "https://github.com") or + * URL (e.g. "https://github.com/zeroclipboard/zeroclipboard/"). + * + * @returns the domain + * @private + */ + var _extractDomain = function(originOrUrl) { + if (originOrUrl == null || originOrUrl === "") { + return null; + } + originOrUrl = originOrUrl.replace(/^\s+|\s+$/g, ""); + if (originOrUrl === "") { + return null; + } + var protocolIndex = originOrUrl.indexOf("//"); + originOrUrl = protocolIndex === -1 ? originOrUrl : originOrUrl.slice(protocolIndex + 2); + var pathIndex = originOrUrl.indexOf("/"); + originOrUrl = pathIndex === -1 ? originOrUrl : protocolIndex === -1 || pathIndex === 0 ? null : originOrUrl.slice(0, pathIndex); + if (originOrUrl && originOrUrl.slice(-4).toLowerCase() === ".swf") { + return null; + } + return originOrUrl || null; + }; + /** + * Set `allowScriptAccess` based on `trustedDomains` and `window.location.host` vs. `swfPath`. + * + * @returns The appropriate script access level. + * @private + */ + var _determineScriptAccess = function() { + var _extractAllDomains = function(origins) { + var i, len, tmp, resultsArray = []; + if (typeof origins === "string") { + origins = [ origins ]; + } + if (!(typeof origins === "object" && origins && typeof origins.length === "number")) { + return resultsArray; + } + for (i = 0, len = origins.length; i < len; i++) { + if (_hasOwn.call(origins, i) && (tmp = _extractDomain(origins[i]))) { + if (tmp === "*") { + resultsArray.length = 0; + resultsArray.push("*"); + break; + } + if (resultsArray.indexOf(tmp) === -1) { + resultsArray.push(tmp); + } + } + } + return resultsArray; + }; + return function(currentDomain, configOptions) { + var swfDomain = _extractDomain(configOptions.swfPath); + if (swfDomain === null) { + swfDomain = currentDomain; + } + var trustedDomains = _extractAllDomains(configOptions.trustedDomains); + var len = trustedDomains.length; + if (len > 0) { + if (len === 1 && trustedDomains[0] === "*") { + return "always"; + } + if (trustedDomains.indexOf(currentDomain) !== -1) { + if (len === 1 && currentDomain === swfDomain) { + return "sameDomain"; + } + return "always"; + } + } + return "never"; + }; + }(); + /** + * Get the currently active/focused DOM element. + * + * @returns the currently active/focused element, or `null` + * @private + */ + var _safeActiveElement = function() { + try { + return _document.activeElement; + } catch (err) { + return null; + } + }; + /** + * Add a class to an element, if it doesn't already have it. + * + * @returns The element, with its new class added. + * @private + */ + var _addClass = function(element, value) { + var c, cl, className, classNames = []; + if (typeof value === "string" && value) { + classNames = value.split(/\s+/); + } + if (element && element.nodeType === 1 && classNames.length > 0) { + if (element.classList) { + for (c = 0, cl = classNames.length; c < cl; c++) { + element.classList.add(classNames[c]); + } + } else if (element.hasOwnProperty("className")) { + className = " " + element.className + " "; + for (c = 0, cl = classNames.length; c < cl; c++) { + if (className.indexOf(" " + classNames[c] + " ") === -1) { + className += classNames[c] + " "; + } + } + element.className = className.replace(/^\s+|\s+$/g, ""); + } + } + return element; + }; + /** + * Remove a class from an element, if it has it. + * + * @returns The element, with its class removed. + * @private + */ + var _removeClass = function(element, value) { + var c, cl, className, classNames = []; + if (typeof value === "string" && value) { + classNames = value.split(/\s+/); + } + if (element && element.nodeType === 1 && classNames.length > 0) { + if (element.classList && element.classList.length > 0) { + for (c = 0, cl = classNames.length; c < cl; c++) { + element.classList.remove(classNames[c]); + } + } else if (element.className) { + className = (" " + element.className + " ").replace(/[\r\n\t]/g, " "); + for (c = 0, cl = classNames.length; c < cl; c++) { + className = className.replace(" " + classNames[c] + " ", " "); + } + element.className = className.replace(/^\s+|\s+$/g, ""); + } + } + return element; + }; + /** + * Attempt to interpret the element's CSS styling. If `prop` is `"cursor"`, + * then we assume that it should be a hand ("pointer") cursor if the element + * is an anchor element ("a" tag). + * + * @returns The computed style property. + * @private + */ + var _getStyle = function(el, prop) { + var value = _getComputedStyle(el, null).getPropertyValue(prop); + if (prop === "cursor") { + if (!value || value === "auto") { + if (el.nodeName === "A") { + return "pointer"; + } + } + } + return value; + }; + /** + * Get the absolutely positioned coordinates of a DOM element. + * + * @returns Object containing the element's position, width, and height. + * @private + */ + var _getElementPosition = function(el) { + var pos = { + left: 0, + top: 0, + width: 0, + height: 0 + }; + if (el.getBoundingClientRect) { + var elRect = el.getBoundingClientRect(); + var pageXOffset = _window.pageXOffset; + var pageYOffset = _window.pageYOffset; + var leftBorderWidth = _document.documentElement.clientLeft || 0; + var topBorderWidth = _document.documentElement.clientTop || 0; + var leftBodyOffset = 0; + var topBodyOffset = 0; + if (_getStyle(_document.body, "position") === "relative") { + var bodyRect = _document.body.getBoundingClientRect(); + var htmlRect = _document.documentElement.getBoundingClientRect(); + leftBodyOffset = bodyRect.left - htmlRect.left || 0; + topBodyOffset = bodyRect.top - htmlRect.top || 0; + } + pos.left = elRect.left + pageXOffset - leftBorderWidth - leftBodyOffset; + pos.top = elRect.top + pageYOffset - topBorderWidth - topBodyOffset; + pos.width = "width" in elRect ? elRect.width : elRect.right - elRect.left; + pos.height = "height" in elRect ? elRect.height : elRect.bottom - elRect.top; + } + return pos; + }; + /** + * Determine is an element is visible somewhere within the document (page). + * + * @returns Boolean + * @private + */ + var _isElementVisible = function(el) { + if (!el) { + return false; + } + var styles = _getComputedStyle(el, null); + var hasCssHeight = _parseFloat(styles.height) > 0; + var hasCssWidth = _parseFloat(styles.width) > 0; + var hasCssTop = _parseFloat(styles.top) >= 0; + var hasCssLeft = _parseFloat(styles.left) >= 0; + var cssKnows = hasCssHeight && hasCssWidth && hasCssTop && hasCssLeft; + var rect = cssKnows ? null : _getElementPosition(el); + var isVisible = styles.display !== "none" && styles.visibility !== "collapse" && (cssKnows || !!rect && (hasCssHeight || rect.height > 0) && (hasCssWidth || rect.width > 0) && (hasCssTop || rect.top >= 0) && (hasCssLeft || rect.left >= 0)); + return isVisible; + }; + /** + * Clear all existing timeouts and interval polling delegates. + * + * @returns `undefined` + * @private + */ + var _clearTimeoutsAndPolling = function() { + _clearTimeout(_flashCheckTimeout); + _flashCheckTimeout = 0; + _clearInterval(_swfFallbackCheckInterval); + _swfFallbackCheckInterval = 0; + }; + /** + * Reposition the Flash object to cover the currently activated element. + * + * @returns `undefined` + * @private + */ + var _reposition = function() { + var htmlBridge; + if (_currentElement && (htmlBridge = _getHtmlBridge(_flashState.bridge))) { + var pos = _getElementPosition(_currentElement); + _extend(htmlBridge.style, { + width: pos.width + "px", + height: pos.height + "px", + top: pos.top + "px", + left: pos.left + "px", + zIndex: "" + _getSafeZIndex(_globalConfig.zIndex) + }); + } + }; + /** + * Sends a signal to the Flash object to display the hand cursor if `true`. + * + * @returns `undefined` + * @private + */ + var _setHandCursor = function(enabled) { + if (_flashState.ready === true) { + if (_flashState.bridge && typeof _flashState.bridge.setHandCursor === "function") { + _flashState.bridge.setHandCursor(enabled); + } else { + _flashState.ready = false; + } + } + }; + /** + * Get a safe value for `zIndex` + * + * @returns an integer, or "auto" + * @private + */ + var _getSafeZIndex = function(val) { + if (/^(?:auto|inherit)$/.test(val)) { + return val; + } + var zIndex; + if (typeof val === "number" && !_isNaN(val)) { + zIndex = val; + } else if (typeof val === "string") { + zIndex = _getSafeZIndex(_parseInt(val, 10)); + } + return typeof zIndex === "number" ? zIndex : "auto"; + }; + /** + * Attempt to detect if ZeroClipboard is executing inside of a sandboxed iframe. + * If it is, Flash Player cannot be used, so ZeroClipboard is dead in the water. + * + * @see {@link http://lists.w3.org/Archives/Public/public-whatwg-archive/2014Dec/0002.html} + * @see {@link https://github.com/zeroclipboard/zeroclipboard/issues/511} + * @see {@link http://zeroclipboard.org/test-iframes.html} + * + * @returns `true` (is sandboxed), `false` (is not sandboxed), or `null` (uncertain) + * @private + */ + var _detectSandbox = function(doNotReassessFlashSupport) { + var effectiveScriptOrigin, frame, frameError, previousState = _flashState.sandboxed, isSandboxed = null; + doNotReassessFlashSupport = doNotReassessFlashSupport === true; + if (_pageIsFramed === false) { + isSandboxed = false; + } else { + try { + frame = window.frameElement || null; + } catch (e) { + frameError = { + name: e.name, + message: e.message + }; + } + if (frame && frame.nodeType === 1 && frame.nodeName === "IFRAME") { + try { + isSandboxed = frame.hasAttribute("sandbox"); + } catch (e) { + isSandboxed = null; + } + } else { + try { + effectiveScriptOrigin = document.domain || null; + } catch (e) { + effectiveScriptOrigin = null; + } + if (effectiveScriptOrigin === null || frameError && frameError.name === "SecurityError" && /(^|[\s\(\[@])sandbox(es|ed|ing|[\s\.,!\)\]@]|$)/.test(frameError.message.toLowerCase())) { + isSandboxed = true; + } + } + } + _flashState.sandboxed = isSandboxed; + if (previousState !== isSandboxed && !doNotReassessFlashSupport) { + _detectFlashSupport(_ActiveXObject); + } + return isSandboxed; + }; + /** + * Detect the Flash Player status, version, and plugin type. + * + * @see {@link https://code.google.com/p/doctype-mirror/wiki/ArticleDetectFlash#The_code} + * @see {@link http://stackoverflow.com/questions/12866060/detecting-pepper-ppapi-flash-with-javascript} + * + * @returns `undefined` + * @private + */ + var _detectFlashSupport = function(ActiveXObject) { + var plugin, ax, mimeType, hasFlash = false, isActiveX = false, isPPAPI = false, flashVersion = ""; + /** + * Derived from Apple's suggested sniffer. + * @param {String} desc e.g. "Shockwave Flash 7.0 r61" + * @returns {String} "7.0.61" + * @private + */ + function parseFlashVersion(desc) { + var matches = desc.match(/[\d]+/g); + matches.length = 3; + return matches.join("."); + } + function isPepperFlash(flashPlayerFileName) { + return !!flashPlayerFileName && (flashPlayerFileName = flashPlayerFileName.toLowerCase()) && (/^(pepflashplayer\.dll|libpepflashplayer\.so|pepperflashplayer\.plugin)$/.test(flashPlayerFileName) || flashPlayerFileName.slice(-13) === "chrome.plugin"); + } + function inspectPlugin(plugin) { + if (plugin) { + hasFlash = true; + if (plugin.version) { + flashVersion = parseFlashVersion(plugin.version); + } + if (!flashVersion && plugin.description) { + flashVersion = parseFlashVersion(plugin.description); + } + if (plugin.filename) { + isPPAPI = isPepperFlash(plugin.filename); + } + } + } + if (_navigator.plugins && _navigator.plugins.length) { + plugin = _navigator.plugins["Shockwave Flash"]; + inspectPlugin(plugin); + if (_navigator.plugins["Shockwave Flash 2.0"]) { + hasFlash = true; + flashVersion = "2.0.0.11"; + } + } else if (_navigator.mimeTypes && _navigator.mimeTypes.length) { + mimeType = _navigator.mimeTypes["application/x-shockwave-flash"]; + plugin = mimeType && mimeType.enabledPlugin; + inspectPlugin(plugin); + } else if (typeof ActiveXObject !== "undefined") { + isActiveX = true; + try { + ax = new ActiveXObject("ShockwaveFlash.ShockwaveFlash.7"); + hasFlash = true; + flashVersion = parseFlashVersion(ax.GetVariable("$version")); + } catch (e1) { + try { + ax = new ActiveXObject("ShockwaveFlash.ShockwaveFlash.6"); + hasFlash = true; + flashVersion = "6.0.21"; + } catch (e2) { + try { + ax = new ActiveXObject("ShockwaveFlash.ShockwaveFlash"); + hasFlash = true; + flashVersion = parseFlashVersion(ax.GetVariable("$version")); + } catch (e3) { + isActiveX = false; + } + } + } + } + _flashState.disabled = hasFlash !== true; + _flashState.outdated = flashVersion && _parseFloat(flashVersion) < _parseFloat(_minimumFlashVersion); + _flashState.version = flashVersion || "0.0.0"; + _flashState.pluginType = isPPAPI ? "pepper" : isActiveX ? "activex" : hasFlash ? "netscape" : "unknown"; + }; + /** + * Invoke the Flash detection algorithms immediately upon inclusion so we're not waiting later. + */ + _detectFlashSupport(_ActiveXObject); + /** + * Always assess the `sandboxed` state of the page at important Flash-related moments. + */ + _detectSandbox(true); + /** + * A shell constructor for `ZeroClipboard` client instances. + * + * @constructor + */ + var ZeroClipboard = function() { + if (!(this instanceof ZeroClipboard)) { + return new ZeroClipboard(); + } + if (typeof ZeroClipboard._createClient === "function") { + ZeroClipboard._createClient.apply(this, _args(arguments)); + } + }; + /** + * The ZeroClipboard library's version number. + * + * @static + * @readonly + * @property {string} + */ + _defineProperty(ZeroClipboard, "version", { + value: "2.2.0", + writable: false, + configurable: true, + enumerable: true + }); + /** + * Update or get a copy of the ZeroClipboard global configuration. + * Returns a copy of the current/updated configuration. + * + * @returns Object + * @static + */ + ZeroClipboard.config = function() { + return _config.apply(this, _args(arguments)); + }; + /** + * Diagnostic method that describes the state of the browser, Flash Player, and ZeroClipboard. + * + * @returns Object + * @static + */ + ZeroClipboard.state = function() { + return _state.apply(this, _args(arguments)); + }; + /** + * Check if Flash is unusable for any reason: disabled, outdated, deactivated, etc. + * + * @returns Boolean + * @static + */ + ZeroClipboard.isFlashUnusable = function() { + return _isFlashUnusable.apply(this, _args(arguments)); + }; + /** + * Register an event listener. + * + * @returns `ZeroClipboard` + * @static + */ + ZeroClipboard.on = function() { + return _on.apply(this, _args(arguments)); + }; + /** + * Unregister an event listener. + * If no `listener` function/object is provided, it will unregister all listeners for the provided `eventType`. + * If no `eventType` is provided, it will unregister all listeners for every event type. + * + * @returns `ZeroClipboard` + * @static + */ + ZeroClipboard.off = function() { + return _off.apply(this, _args(arguments)); + }; + /** + * Retrieve event listeners for an `eventType`. + * If no `eventType` is provided, it will retrieve all listeners for every event type. + * + * @returns array of listeners for the `eventType`; if no `eventType`, then a map/hash object of listeners for all event types; or `null` + */ + ZeroClipboard.handlers = function() { + return _listeners.apply(this, _args(arguments)); + }; + /** + * Event emission receiver from the Flash object, forwarding to any registered JavaScript event listeners. + * + * @returns For the "copy" event, returns the Flash-friendly "clipData" object; otherwise `undefined`. + * @static + */ + ZeroClipboard.emit = function() { + return _emit.apply(this, _args(arguments)); + }; + /** + * Create and embed the Flash object. + * + * @returns The Flash object + * @static + */ + ZeroClipboard.create = function() { + return _create.apply(this, _args(arguments)); + }; + /** + * Self-destruct and clean up everything, including the embedded Flash object. + * + * @returns `undefined` + * @static + */ + ZeroClipboard.destroy = function() { + return _destroy.apply(this, _args(arguments)); + }; + /** + * Set the pending data for clipboard injection. + * + * @returns `undefined` + * @static + */ + ZeroClipboard.setData = function() { + return _setData.apply(this, _args(arguments)); + }; + /** + * Clear the pending data for clipboard injection. + * If no `format` is provided, all pending data formats will be cleared. + * + * @returns `undefined` + * @static + */ + ZeroClipboard.clearData = function() { + return _clearData.apply(this, _args(arguments)); + }; + /** + * Get a copy of the pending data for clipboard injection. + * If no `format` is provided, a copy of ALL pending data formats will be returned. + * + * @returns `String` or `Object` + * @static + */ + ZeroClipboard.getData = function() { + return _getData.apply(this, _args(arguments)); + }; + /** + * Sets the current HTML object that the Flash object should overlay. This will put the global + * Flash object on top of the current element; depending on the setup, this may also set the + * pending clipboard text data as well as the Flash object's wrapping element's title attribute + * based on the underlying HTML element and ZeroClipboard configuration. + * + * @returns `undefined` + * @static + */ + ZeroClipboard.focus = ZeroClipboard.activate = function() { + return _focus.apply(this, _args(arguments)); + }; + /** + * Un-overlays the Flash object. This will put the global Flash object off-screen; depending on + * the setup, this may also unset the Flash object's wrapping element's title attribute based on + * the underlying HTML element and ZeroClipboard configuration. + * + * @returns `undefined` + * @static + */ + ZeroClipboard.blur = ZeroClipboard.deactivate = function() { + return _blur.apply(this, _args(arguments)); + }; + /** + * Returns the currently focused/"activated" HTML element that the Flash object is wrapping. + * + * @returns `HTMLElement` or `null` + * @static + */ + ZeroClipboard.activeElement = function() { + return _activeElement.apply(this, _args(arguments)); + }; + /** + * Keep track of the ZeroClipboard client instance counter. + */ + var _clientIdCounter = 0; + /** + * Keep track of the state of the client instances. + * + * Entry structure: + * _clientMeta[client.id] = { + * instance: client, + * elements: [], + * handlers: {} + * }; + */ + var _clientMeta = {}; + /** + * Keep track of the ZeroClipboard clipped elements counter. + */ + var _elementIdCounter = 0; + /** + * Keep track of the state of the clipped element relationships to clients. + * + * Entry structure: + * _elementMeta[element.zcClippingId] = [client1.id, client2.id]; + */ + var _elementMeta = {}; + /** + * Keep track of the state of the mouse event handlers for clipped elements. + * + * Entry structure: + * _mouseHandlers[element.zcClippingId] = { + * mouseover: function(event) {}, + * mouseout: function(event) {}, + * mouseenter: function(event) {}, + * mouseleave: function(event) {}, + * mousemove: function(event) {} + * }; + */ + var _mouseHandlers = {}; + /** + * Extending the ZeroClipboard configuration defaults for the Client module. + */ + _extend(_globalConfig, { + autoActivate: true + }); + /** + * The real constructor for `ZeroClipboard` client instances. + * @private + */ + var _clientConstructor = function(elements) { + var client = this; + client.id = "" + _clientIdCounter++; + _clientMeta[client.id] = { + instance: client, + elements: [], + handlers: {} + }; + if (elements) { + client.clip(elements); + } + ZeroClipboard.on("*", function(event) { + return client.emit(event); + }); + ZeroClipboard.on("destroy", function() { + client.destroy(); + }); + ZeroClipboard.create(); + }; + /** + * The underlying implementation of `ZeroClipboard.Client.prototype.on`. + * @private + */ + var _clientOn = function(eventType, listener) { + var i, len, events, added = {}, meta = _clientMeta[this.id], handlers = meta && meta.handlers; + if (!meta) { + throw new Error("Attempted to add new listener(s) to a destroyed ZeroClipboard client instance"); + } + if (typeof eventType === "string" && eventType) { + events = eventType.toLowerCase().split(/\s+/); + } else if (typeof eventType === "object" && eventType && typeof listener === "undefined") { + for (i in eventType) { + if (_hasOwn.call(eventType, i) && typeof i === "string" && i && typeof eventType[i] === "function") { + this.on(i, eventType[i]); + } + } + } + if (events && events.length) { + for (i = 0, len = events.length; i < len; i++) { + eventType = events[i].replace(/^on/, ""); + added[eventType] = true; + if (!handlers[eventType]) { + handlers[eventType] = []; + } + handlers[eventType].push(listener); + } + if (added.ready && _flashState.ready) { + this.emit({ + type: "ready", + client: this + }); + } + if (added.error) { + for (i = 0, len = _flashStateErrorNames.length; i < len; i++) { + if (_flashState[_flashStateErrorNames[i].replace(/^flash-/, "")]) { + this.emit({ + type: "error", + name: _flashStateErrorNames[i], + client: this + }); + break; + } + } + if (_zcSwfVersion !== undefined && ZeroClipboard.version !== _zcSwfVersion) { + this.emit({ + type: "error", + name: "version-mismatch", + jsVersion: ZeroClipboard.version, + swfVersion: _zcSwfVersion + }); + } + } + } + return this; + }; + /** + * The underlying implementation of `ZeroClipboard.Client.prototype.off`. + * @private + */ + var _clientOff = function(eventType, listener) { + var i, len, foundIndex, events, perEventHandlers, meta = _clientMeta[this.id], handlers = meta && meta.handlers; + if (!handlers) { + return this; + } + if (arguments.length === 0) { + events = _keys(handlers); + } else if (typeof eventType === "string" && eventType) { + events = eventType.split(/\s+/); + } else if (typeof eventType === "object" && eventType && typeof listener === "undefined") { + for (i in eventType) { + if (_hasOwn.call(eventType, i) && typeof i === "string" && i && typeof eventType[i] === "function") { + this.off(i, eventType[i]); + } + } + } + if (events && events.length) { + for (i = 0, len = events.length; i < len; i++) { + eventType = events[i].toLowerCase().replace(/^on/, ""); + perEventHandlers = handlers[eventType]; + if (perEventHandlers && perEventHandlers.length) { + if (listener) { + foundIndex = perEventHandlers.indexOf(listener); + while (foundIndex !== -1) { + perEventHandlers.splice(foundIndex, 1); + foundIndex = perEventHandlers.indexOf(listener, foundIndex); + } + } else { + perEventHandlers.length = 0; + } + } + } + } + return this; + }; + /** + * The underlying implementation of `ZeroClipboard.Client.prototype.handlers`. + * @private + */ + var _clientListeners = function(eventType) { + var copy = null, handlers = _clientMeta[this.id] && _clientMeta[this.id].handlers; + if (handlers) { + if (typeof eventType === "string" && eventType) { + copy = handlers[eventType] ? handlers[eventType].slice(0) : []; + } else { + copy = _deepCopy(handlers); + } + } + return copy; + }; + /** + * The underlying implementation of `ZeroClipboard.Client.prototype.emit`. + * @private + */ + var _clientEmit = function(event) { + if (_clientShouldEmit.call(this, event)) { + if (typeof event === "object" && event && typeof event.type === "string" && event.type) { + event = _extend({}, event); + } + var eventCopy = _extend({}, _createEvent(event), { + client: this + }); + _clientDispatchCallbacks.call(this, eventCopy); + } + return this; + }; + /** + * The underlying implementation of `ZeroClipboard.Client.prototype.clip`. + * @private + */ + var _clientClip = function(elements) { + if (!_clientMeta[this.id]) { + throw new Error("Attempted to clip element(s) to a destroyed ZeroClipboard client instance"); + } + elements = _prepClip(elements); + for (var i = 0; i < elements.length; i++) { + if (_hasOwn.call(elements, i) && elements[i] && elements[i].nodeType === 1) { + if (!elements[i].zcClippingId) { + elements[i].zcClippingId = "zcClippingId_" + _elementIdCounter++; + _elementMeta[elements[i].zcClippingId] = [ this.id ]; + if (_globalConfig.autoActivate === true) { + _addMouseHandlers(elements[i]); + } + } else if (_elementMeta[elements[i].zcClippingId].indexOf(this.id) === -1) { + _elementMeta[elements[i].zcClippingId].push(this.id); + } + var clippedElements = _clientMeta[this.id] && _clientMeta[this.id].elements; + if (clippedElements.indexOf(elements[i]) === -1) { + clippedElements.push(elements[i]); + } + } + } + return this; + }; + /** + * The underlying implementation of `ZeroClipboard.Client.prototype.unclip`. + * @private + */ + var _clientUnclip = function(elements) { + var meta = _clientMeta[this.id]; + if (!meta) { + return this; + } + var clippedElements = meta.elements; + var arrayIndex; + if (typeof elements === "undefined") { + elements = clippedElements.slice(0); + } else { + elements = _prepClip(elements); + } + for (var i = elements.length; i--; ) { + if (_hasOwn.call(elements, i) && elements[i] && elements[i].nodeType === 1) { + arrayIndex = 0; + while ((arrayIndex = clippedElements.indexOf(elements[i], arrayIndex)) !== -1) { + clippedElements.splice(arrayIndex, 1); + } + var clientIds = _elementMeta[elements[i].zcClippingId]; + if (clientIds) { + arrayIndex = 0; + while ((arrayIndex = clientIds.indexOf(this.id, arrayIndex)) !== -1) { + clientIds.splice(arrayIndex, 1); + } + if (clientIds.length === 0) { + if (_globalConfig.autoActivate === true) { + _removeMouseHandlers(elements[i]); + } + delete elements[i].zcClippingId; + } + } + } + } + return this; + }; + /** + * The underlying implementation of `ZeroClipboard.Client.prototype.elements`. + * @private + */ + var _clientElements = function() { + var meta = _clientMeta[this.id]; + return meta && meta.elements ? meta.elements.slice(0) : []; + }; + /** + * The underlying implementation of `ZeroClipboard.Client.prototype.destroy`. + * @private + */ + var _clientDestroy = function() { + if (!_clientMeta[this.id]) { + return; + } + this.unclip(); + this.off(); + delete _clientMeta[this.id]; + }; + /** + * Inspect an Event to see if the Client (`this`) should honor it for emission. + * @private + */ + var _clientShouldEmit = function(event) { + if (!(event && event.type)) { + return false; + } + if (event.client && event.client !== this) { + return false; + } + var meta = _clientMeta[this.id]; + var clippedEls = meta && meta.elements; + var hasClippedEls = !!clippedEls && clippedEls.length > 0; + var goodTarget = !event.target || hasClippedEls && clippedEls.indexOf(event.target) !== -1; + var goodRelTarget = event.relatedTarget && hasClippedEls && clippedEls.indexOf(event.relatedTarget) !== -1; + var goodClient = event.client && event.client === this; + if (!meta || !(goodTarget || goodRelTarget || goodClient)) { + return false; + } + return true; + }; + /** + * Handle the actual dispatching of events to a client instance. + * + * @returns `undefined` + * @private + */ + var _clientDispatchCallbacks = function(event) { + var meta = _clientMeta[this.id]; + if (!(typeof event === "object" && event && event.type && meta)) { + return; + } + var async = _shouldPerformAsync(event); + var wildcardTypeHandlers = meta && meta.handlers["*"] || []; + var specificTypeHandlers = meta && meta.handlers[event.type] || []; + var handlers = wildcardTypeHandlers.concat(specificTypeHandlers); + if (handlers && handlers.length) { + var i, len, func, context, eventCopy, originalContext = this; + for (i = 0, len = handlers.length; i < len; i++) { + func = handlers[i]; + context = originalContext; + if (typeof func === "string" && typeof _window[func] === "function") { + func = _window[func]; + } + if (typeof func === "object" && func && typeof func.handleEvent === "function") { + context = func; + func = func.handleEvent; + } + if (typeof func === "function") { + eventCopy = _extend({}, event); + _dispatchCallback(func, context, [ eventCopy ], async); + } + } + } + }; + /** + * Prepares the elements for clipping/unclipping. + * + * @returns An Array of elements. + * @private + */ + var _prepClip = function(elements) { + if (typeof elements === "string") { + elements = []; + } + return typeof elements.length !== "number" ? [ elements ] : elements; + }; + /** + * Add a `mouseover` handler function for a clipped element. + * + * @returns `undefined` + * @private + */ + var _addMouseHandlers = function(element) { + if (!(element && element.nodeType === 1)) { + return; + } + var _suppressMouseEvents = function(event) { + if (!(event || (event = _window.event))) { + return; + } + if (event._source !== "js") { + event.stopImmediatePropagation(); + event.preventDefault(); + } + delete event._source; + }; + var _elementMouseOver = function(event) { + if (!(event || (event = _window.event))) { + return; + } + _suppressMouseEvents(event); + ZeroClipboard.focus(element); + }; + element.addEventListener("mouseover", _elementMouseOver, false); + element.addEventListener("mouseout", _suppressMouseEvents, false); + element.addEventListener("mouseenter", _suppressMouseEvents, false); + element.addEventListener("mouseleave", _suppressMouseEvents, false); + element.addEventListener("mousemove", _suppressMouseEvents, false); + _mouseHandlers[element.zcClippingId] = { + mouseover: _elementMouseOver, + mouseout: _suppressMouseEvents, + mouseenter: _suppressMouseEvents, + mouseleave: _suppressMouseEvents, + mousemove: _suppressMouseEvents + }; + }; + /** + * Remove a `mouseover` handler function for a clipped element. + * + * @returns `undefined` + * @private + */ + var _removeMouseHandlers = function(element) { + if (!(element && element.nodeType === 1)) { + return; + } + var mouseHandlers = _mouseHandlers[element.zcClippingId]; + if (!(typeof mouseHandlers === "object" && mouseHandlers)) { + return; + } + var key, val, mouseEvents = [ "move", "leave", "enter", "out", "over" ]; + for (var i = 0, len = mouseEvents.length; i < len; i++) { + key = "mouse" + mouseEvents[i]; + val = mouseHandlers[key]; + if (typeof val === "function") { + element.removeEventListener(key, val, false); + } + } + delete _mouseHandlers[element.zcClippingId]; + }; + /** + * Creates a new ZeroClipboard client instance. + * Optionally, auto-`clip` an element or collection of elements. + * + * @constructor + */ + ZeroClipboard._createClient = function() { + _clientConstructor.apply(this, _args(arguments)); + }; + /** + * Register an event listener to the client. + * + * @returns `this` + */ + ZeroClipboard.prototype.on = function() { + return _clientOn.apply(this, _args(arguments)); + }; + /** + * Unregister an event handler from the client. + * If no `listener` function/object is provided, it will unregister all handlers for the provided `eventType`. + * If no `eventType` is provided, it will unregister all handlers for every event type. + * + * @returns `this` + */ + ZeroClipboard.prototype.off = function() { + return _clientOff.apply(this, _args(arguments)); + }; + /** + * Retrieve event listeners for an `eventType` from the client. + * If no `eventType` is provided, it will retrieve all listeners for every event type. + * + * @returns array of listeners for the `eventType`; if no `eventType`, then a map/hash object of listeners for all event types; or `null` + */ + ZeroClipboard.prototype.handlers = function() { + return _clientListeners.apply(this, _args(arguments)); + }; + /** + * Event emission receiver from the Flash object for this client's registered JavaScript event listeners. + * + * @returns For the "copy" event, returns the Flash-friendly "clipData" object; otherwise `undefined`. + */ + ZeroClipboard.prototype.emit = function() { + return _clientEmit.apply(this, _args(arguments)); + }; + /** + * Register clipboard actions for new element(s) to the client. + * + * @returns `this` + */ + ZeroClipboard.prototype.clip = function() { + return _clientClip.apply(this, _args(arguments)); + }; + /** + * Unregister the clipboard actions of previously registered element(s) on the page. + * If no elements are provided, ALL registered elements will be unregistered. + * + * @returns `this` + */ + ZeroClipboard.prototype.unclip = function() { + return _clientUnclip.apply(this, _args(arguments)); + }; + /** + * Get all of the elements to which this client is clipped. + * + * @returns array of clipped elements + */ + ZeroClipboard.prototype.elements = function() { + return _clientElements.apply(this, _args(arguments)); + }; + /** + * Self-destruct and clean up everything for a single client. + * This will NOT destroy the embedded Flash object. + * + * @returns `undefined` + */ + ZeroClipboard.prototype.destroy = function() { + return _clientDestroy.apply(this, _args(arguments)); + }; + /** + * Stores the pending plain text to inject into the clipboard. + * + * @returns `this` + */ + ZeroClipboard.prototype.setText = function(text) { + if (!_clientMeta[this.id]) { + throw new Error("Attempted to set pending clipboard data from a destroyed ZeroClipboard client instance"); + } + ZeroClipboard.setData("text/plain", text); + return this; + }; + /** + * Stores the pending HTML text to inject into the clipboard. + * + * @returns `this` + */ + ZeroClipboard.prototype.setHtml = function(html) { + if (!_clientMeta[this.id]) { + throw new Error("Attempted to set pending clipboard data from a destroyed ZeroClipboard client instance"); + } + ZeroClipboard.setData("text/html", html); + return this; + }; + /** + * Stores the pending rich text (RTF) to inject into the clipboard. + * + * @returns `this` + */ + ZeroClipboard.prototype.setRichText = function(richText) { + if (!_clientMeta[this.id]) { + throw new Error("Attempted to set pending clipboard data from a destroyed ZeroClipboard client instance"); + } + ZeroClipboard.setData("application/rtf", richText); + return this; + }; + /** + * Stores the pending data to inject into the clipboard. + * + * @returns `this` + */ + ZeroClipboard.prototype.setData = function() { + if (!_clientMeta[this.id]) { + throw new Error("Attempted to set pending clipboard data from a destroyed ZeroClipboard client instance"); + } + ZeroClipboard.setData.apply(this, _args(arguments)); + return this; + }; + /** + * Clears the pending data to inject into the clipboard. + * If no `format` is provided, all pending data formats will be cleared. + * + * @returns `this` + */ + ZeroClipboard.prototype.clearData = function() { + if (!_clientMeta[this.id]) { + throw new Error("Attempted to clear pending clipboard data from a destroyed ZeroClipboard client instance"); + } + ZeroClipboard.clearData.apply(this, _args(arguments)); + return this; + }; + /** + * Gets a copy of the pending data to inject into the clipboard. + * If no `format` is provided, a copy of ALL pending data formats will be returned. + * + * @returns `String` or `Object` + */ + ZeroClipboard.prototype.getData = function() { + if (!_clientMeta[this.id]) { + throw new Error("Attempted to get pending clipboard data from a destroyed ZeroClipboard client instance"); + } + return ZeroClipboard.getData.apply(this, _args(arguments)); + }; + if (typeof define === "function" && define.amd) { + define(function() { + return ZeroClipboard; + }); + } else if (typeof module === "object" && module && typeof module.exports === "object" && module.exports) { + module.exports = ZeroClipboard; + } else { + window.ZeroClipboard = ZeroClipboard; + } +})(function() { + return this || window; +}()); \ No newline at end of file diff --git a/bower_components/zeroclipboard/dist/ZeroClipboard.min.js b/bower_components/zeroclipboard/dist/ZeroClipboard.min.js new file mode 100644 index 0000000..6ecd088 --- /dev/null +++ b/bower_components/zeroclipboard/dist/ZeroClipboard.min.js @@ -0,0 +1,10 @@ +/*! + * ZeroClipboard + * The ZeroClipboard library provides an easy way to copy text to the clipboard using an invisible Adobe Flash movie and a JavaScript interface. + * Copyright (c) 2009-2014 Jon Rohan, James M. Greene + * Licensed MIT + * http://zeroclipboard.org/ + * v2.2.0 + */ +!function(a,b){"use strict";var c,d,e,f=a,g=f.document,h=f.navigator,i=f.setTimeout,j=f.clearTimeout,k=f.setInterval,l=f.clearInterval,m=f.getComputedStyle,n=f.encodeURIComponent,o=f.ActiveXObject,p=f.Error,q=f.Number.parseInt||f.parseInt,r=f.Number.parseFloat||f.parseFloat,s=f.Number.isNaN||f.isNaN,t=f.Date.now,u=f.Object.keys,v=f.Object.defineProperty,w=f.Object.prototype.hasOwnProperty,x=f.Array.prototype.slice,y=function(){var a=function(a){return a};if("function"==typeof f.wrap&&"function"==typeof f.unwrap)try{var b=g.createElement("div"),c=f.unwrap(b);1===b.nodeType&&c&&1===c.nodeType&&(a=f.unwrap)}catch(d){}return a}(),z=function(a){return x.call(a,0)},A=function(){var a,c,d,e,f,g,h=z(arguments),i=h[0]||{};for(a=1,c=h.length;c>a;a++)if(null!=(d=h[a]))for(e in d)w.call(d,e)&&(f=i[e],g=d[e],i!==g&&g!==b&&(i[e]=g));return i},B=function(a){var b,c,d,e;if("object"!=typeof a||null==a||"number"==typeof a.nodeType)b=a;else if("number"==typeof a.length)for(b=[],c=0,d=a.length;d>c;c++)w.call(a,c)&&(b[c]=B(a[c]));else{b={};for(e in a)w.call(a,e)&&(b[e]=B(a[e]))}return b},C=function(a,b){for(var c={},d=0,e=b.length;e>d;d++)b[d]in a&&(c[b[d]]=a[b[d]]);return c},D=function(a,b){var c={};for(var d in a)-1===b.indexOf(d)&&(c[d]=a[d]);return c},E=function(a){if(a)for(var b in a)w.call(a,b)&&delete a[b];return a},F=function(a,b){if(a&&1===a.nodeType&&a.ownerDocument&&b&&(1===b.nodeType&&b.ownerDocument&&b.ownerDocument===a.ownerDocument||9===b.nodeType&&!b.ownerDocument&&b===a.ownerDocument))do{if(a===b)return!0;a=a.parentNode}while(a);return!1},G=function(a){var b;return"string"==typeof a&&a&&(b=a.split("#")[0].split("?")[0],b=a.slice(0,a.lastIndexOf("/")+1)),b},H=function(a){var b,c;return"string"==typeof a&&a&&(c=a.match(/^(?:|[^:@]*@|.+\)@(?=http[s]?|file)|.+?\s+(?: at |@)(?:[^:\(]+ )*[\(]?)((?:http[s]?|file):\/\/[\/]?.+?\/[^:\)]*?)(?::\d+)(?::\d+)?/),c&&c[1]?b=c[1]:(c=a.match(/\)@((?:http[s]?|file):\/\/[\/]?.+?\/[^:\)]*?)(?::\d+)(?::\d+)?/),c&&c[1]&&(b=c[1]))),b},I=function(){var a,b;try{throw new p}catch(c){b=c}return b&&(a=b.sourceURL||b.fileName||H(b.stack)),a},J=function(){var a,c,d;if(g.currentScript&&(a=g.currentScript.src))return a;if(c=g.getElementsByTagName("script"),1===c.length)return c[0].src||b;if("readyState"in c[0])for(d=c.length;d--;)if("interactive"===c[d].readyState&&(a=c[d].src))return a;return"loading"===g.readyState&&(a=c[c.length-1].src)?a:(a=I())?a:b},K=function(){var a,c,d,e=g.getElementsByTagName("script");for(a=e.length;a--;){if(!(d=e[a].src)){c=null;break}if(d=G(d),null==c)c=d;else if(c!==d){c=null;break}}return c||b},L=function(){var a=G(J())||K()||"";return a+"ZeroClipboard.swf"},M=function(){return null==a.opener&&(!!a.top&&a!=a.top||!!a.parent&&a!=a.parent)}(),N={bridge:null,version:"0.0.0",pluginType:"unknown",disabled:null,outdated:null,sandboxed:null,unavailable:null,degraded:null,deactivated:null,overdue:null,ready:null},O="11.0.0",P={},Q={},R=null,S=0,T=0,U={ready:"Flash communication is established",error:{"flash-disabled":"Flash is disabled or not installed. May also be attempting to run Flash in a sandboxed iframe, which is impossible.","flash-outdated":"Flash is too outdated to support ZeroClipboard","flash-sandboxed":"Attempting to run Flash in a sandboxed iframe, which is impossible","flash-unavailable":"Flash is unable to communicate bidirectionally with JavaScript","flash-degraded":"Flash is unable to preserve data fidelity when communicating with JavaScript","flash-deactivated":"Flash is too outdated for your browser and/or is configured as click-to-activate.\nThis may also mean that the ZeroClipboard SWF object could not be loaded, so please check your `swfPath` configuration and/or network connectivity.\nMay also be attempting to run Flash in a sandboxed iframe, which is impossible.","flash-overdue":"Flash communication was established but NOT within the acceptable time limit","version-mismatch":"ZeroClipboard JS version number does not match ZeroClipboard SWF version number","clipboard-error":"At least one error was thrown while ZeroClipboard was attempting to inject your data into the clipboard","config-mismatch":"ZeroClipboard configuration does not match Flash's reality","swf-not-found":"The ZeroClipboard SWF object could not be loaded, so please check your `swfPath` configuration and/or network connectivity"}},V=["flash-unavailable","flash-degraded","flash-overdue","version-mismatch","config-mismatch","clipboard-error"],W=["flash-disabled","flash-outdated","flash-sandboxed","flash-unavailable","flash-degraded","flash-deactivated","flash-overdue"],X=new RegExp("^flash-("+W.map(function(a){return a.replace(/^flash-/,"")}).join("|")+")$"),Y=new RegExp("^flash-("+W.slice(1).map(function(a){return a.replace(/^flash-/,"")}).join("|")+")$"),Z={swfPath:L(),trustedDomains:a.location.host?[a.location.host]:[],cacheBust:!0,forceEnhancedClipboard:!1,flashLoadTimeout:3e4,autoActivate:!0,bubbleEvents:!0,containerId:"global-zeroclipboard-html-bridge",containerClass:"global-zeroclipboard-container",swfObjectId:"global-zeroclipboard-flash-bridge",hoverClass:"zeroclipboard-is-hover",activeClass:"zeroclipboard-is-active",forceHandCursor:!1,title:null,zIndex:999999999},$=function(a){if("object"==typeof a&&null!==a)for(var b in a)if(w.call(a,b))if(/^(?:forceHandCursor|title|zIndex|bubbleEvents)$/.test(b))Z[b]=a[b];else if(null==N.bridge)if("containerId"===b||"swfObjectId"===b){if(!nb(a[b]))throw new Error("The specified `"+b+"` value is not valid as an HTML4 Element ID");Z[b]=a[b]}else Z[b]=a[b];{if("string"!=typeof a||!a)return B(Z);if(w.call(Z,a))return Z[a]}},_=function(){return Tb(),{browser:C(h,["userAgent","platform","appName"]),flash:D(N,["bridge"]),zeroclipboard:{version:Vb.version,config:Vb.config()}}},ab=function(){return!!(N.disabled||N.outdated||N.sandboxed||N.unavailable||N.degraded||N.deactivated)},bb=function(a,d){var e,f,g,h={};if("string"==typeof a&&a)g=a.toLowerCase().split(/\s+/);else if("object"==typeof a&&a&&"undefined"==typeof d)for(e in a)w.call(a,e)&&"string"==typeof e&&e&&"function"==typeof a[e]&&Vb.on(e,a[e]);if(g&&g.length){for(e=0,f=g.length;f>e;e++)a=g[e].replace(/^on/,""),h[a]=!0,P[a]||(P[a]=[]),P[a].push(d);if(h.ready&&N.ready&&Vb.emit({type:"ready"}),h.error){for(e=0,f=W.length;f>e;e++)if(N[W[e].replace(/^flash-/,"")]===!0){Vb.emit({type:"error",name:W[e]});break}c!==b&&Vb.version!==c&&Vb.emit({type:"error",name:"version-mismatch",jsVersion:Vb.version,swfVersion:c})}}return Vb},cb=function(a,b){var c,d,e,f,g;if(0===arguments.length)f=u(P);else if("string"==typeof a&&a)f=a.split(/\s+/);else if("object"==typeof a&&a&&"undefined"==typeof b)for(c in a)w.call(a,c)&&"string"==typeof c&&c&&"function"==typeof a[c]&&Vb.off(c,a[c]);if(f&&f.length)for(c=0,d=f.length;d>c;c++)if(a=f[c].toLowerCase().replace(/^on/,""),g=P[a],g&&g.length)if(b)for(e=g.indexOf(b);-1!==e;)g.splice(e,1),e=g.indexOf(b,e);else g.length=0;return Vb},db=function(a){var b;return b="string"==typeof a&&a?B(P[a])||null:B(P)},eb=function(a){var b,c,d;return a=ob(a),a&&!vb(a)?"ready"===a.type&&N.overdue===!0?Vb.emit({type:"error",name:"flash-overdue"}):(b=A({},a),tb.call(this,b),"copy"===a.type&&(d=Db(Q),c=d.data,R=d.formatMap),c):void 0},fb=function(){var a=N.sandboxed;if(Tb(),"boolean"!=typeof N.ready&&(N.ready=!1),N.sandboxed!==a&&N.sandboxed===!0)N.ready=!1,Vb.emit({type:"error",name:"flash-sandboxed"});else if(!Vb.isFlashUnusable()&&null===N.bridge){var b=Z.flashLoadTimeout;"number"==typeof b&&b>=0&&(S=i(function(){"boolean"!=typeof N.deactivated&&(N.deactivated=!0),N.deactivated===!0&&Vb.emit({type:"error",name:"flash-deactivated"})},b)),N.overdue=!1,Bb()}},gb=function(){Vb.clearData(),Vb.blur(),Vb.emit("destroy"),Cb(),Vb.off()},hb=function(a,b){var c;if("object"==typeof a&&a&&"undefined"==typeof b)c=a,Vb.clearData();else{if("string"!=typeof a||!a)return;c={},c[a]=b}for(var d in c)"string"==typeof d&&d&&w.call(c,d)&&"string"==typeof c[d]&&c[d]&&(Q[d]=c[d])},ib=function(a){"undefined"==typeof a?(E(Q),R=null):"string"==typeof a&&w.call(Q,a)&&delete Q[a]},jb=function(a){return"undefined"==typeof a?B(Q):"string"==typeof a&&w.call(Q,a)?Q[a]:void 0},kb=function(a){if(a&&1===a.nodeType){d&&(Lb(d,Z.activeClass),d!==a&&Lb(d,Z.hoverClass)),d=a,Kb(a,Z.hoverClass);var b=a.getAttribute("title")||Z.title;if("string"==typeof b&&b){var c=Ab(N.bridge);c&&c.setAttribute("title",b)}var e=Z.forceHandCursor===!0||"pointer"===Mb(a,"cursor");Rb(e),Qb()}},lb=function(){var a=Ab(N.bridge);a&&(a.removeAttribute("title"),a.style.left="0px",a.style.top="-9999px",a.style.width="1px",a.style.height="1px"),d&&(Lb(d,Z.hoverClass),Lb(d,Z.activeClass),d=null)},mb=function(){return d||null},nb=function(a){return"string"==typeof a&&a&&/^[A-Za-z][A-Za-z0-9_:\-\.]*$/.test(a)},ob=function(a){var b;if("string"==typeof a&&a?(b=a,a={}):"object"==typeof a&&a&&"string"==typeof a.type&&a.type&&(b=a.type),b){b=b.toLowerCase(),!a.target&&(/^(copy|aftercopy|_click)$/.test(b)||"error"===b&&"clipboard-error"===a.name)&&(a.target=e),A(a,{type:b,target:a.target||d||null,relatedTarget:a.relatedTarget||null,currentTarget:N&&N.bridge||null,timeStamp:a.timeStamp||t()||null});var c=U[a.type];return"error"===a.type&&a.name&&c&&(c=c[a.name]),c&&(a.message=c),"ready"===a.type&&A(a,{target:null,version:N.version}),"error"===a.type&&(X.test(a.name)&&A(a,{target:null,minimumVersion:O}),Y.test(a.name)&&A(a,{version:N.version})),"copy"===a.type&&(a.clipboardData={setData:Vb.setData,clearData:Vb.clearData}),"aftercopy"===a.type&&(a=Eb(a,R)),a.target&&!a.relatedTarget&&(a.relatedTarget=pb(a.target)),qb(a)}},pb=function(a){var b=a&&a.getAttribute&&a.getAttribute("data-clipboard-target");return b?g.getElementById(b):null},qb=function(a){if(a&&/^_(?:click|mouse(?:over|out|down|up|move))$/.test(a.type)){var c=a.target,d="_mouseover"===a.type&&a.relatedTarget?a.relatedTarget:b,e="_mouseout"===a.type&&a.relatedTarget?a.relatedTarget:b,h=Nb(c),i=f.screenLeft||f.screenX||0,j=f.screenTop||f.screenY||0,k=g.body.scrollLeft+g.documentElement.scrollLeft,l=g.body.scrollTop+g.documentElement.scrollTop,m=h.left+("number"==typeof a._stageX?a._stageX:0),n=h.top+("number"==typeof a._stageY?a._stageY:0),o=m-k,p=n-l,q=i+o,r=j+p,s="number"==typeof a.movementX?a.movementX:0,t="number"==typeof a.movementY?a.movementY:0;delete a._stageX,delete a._stageY,A(a,{srcElement:c,fromElement:d,toElement:e,screenX:q,screenY:r,pageX:m,pageY:n,clientX:o,clientY:p,x:o,y:p,movementX:s,movementY:t,offsetX:0,offsetY:0,layerX:0,layerY:0})}return a},rb=function(a){var b=a&&"string"==typeof a.type&&a.type||"";return!/^(?:(?:before)?copy|destroy)$/.test(b)},sb=function(a,b,c,d){d?i(function(){a.apply(b,c)},0):a.apply(b,c)},tb=function(a){if("object"==typeof a&&a&&a.type){var b=rb(a),c=P["*"]||[],d=P[a.type]||[],e=c.concat(d);if(e&&e.length){var g,h,i,j,k,l=this;for(g=0,h=e.length;h>g;g++)i=e[g],j=l,"string"==typeof i&&"function"==typeof f[i]&&(i=f[i]),"object"==typeof i&&i&&"function"==typeof i.handleEvent&&(j=i,i=i.handleEvent),"function"==typeof i&&(k=A({},a),sb(i,j,[k],b))}return this}},ub=function(a){var b=null;return(M===!1||a&&"error"===a.type&&a.name&&-1!==V.indexOf(a.name))&&(b=!1),b},vb=function(a){var b=a.target||d||null,f="swf"===a._source;switch(delete a._source,a.type){case"error":var g="flash-sandboxed"===a.name||ub(a);"boolean"==typeof g&&(N.sandboxed=g),-1!==W.indexOf(a.name)?A(N,{disabled:"flash-disabled"===a.name,outdated:"flash-outdated"===a.name,unavailable:"flash-unavailable"===a.name,degraded:"flash-degraded"===a.name,deactivated:"flash-deactivated"===a.name,overdue:"flash-overdue"===a.name,ready:!1}):"version-mismatch"===a.name&&(c=a.swfVersion,A(N,{disabled:!1,outdated:!1,unavailable:!1,degraded:!1,deactivated:!1,overdue:!1,ready:!1})),Pb();break;case"ready":c=a.swfVersion;var h=N.deactivated===!0;A(N,{disabled:!1,outdated:!1,sandboxed:!1,unavailable:!1,degraded:!1,deactivated:!1,overdue:h,ready:!h}),Pb();break;case"beforecopy":e=b;break;case"copy":var i,j,k=a.relatedTarget;!Q["text/html"]&&!Q["text/plain"]&&k&&(j=k.value||k.outerHTML||k.innerHTML)&&(i=k.value||k.textContent||k.innerText)?(a.clipboardData.clearData(),a.clipboardData.setData("text/plain",i),j!==i&&a.clipboardData.setData("text/html",j)):!Q["text/plain"]&&a.target&&(i=a.target.getAttribute("data-clipboard-text"))&&(a.clipboardData.clearData(),a.clipboardData.setData("text/plain",i));break;case"aftercopy":wb(a),Vb.clearData(),b&&b!==Jb()&&b.focus&&b.focus();break;case"_mouseover":Vb.focus(b),Z.bubbleEvents===!0&&f&&(b&&b!==a.relatedTarget&&!F(a.relatedTarget,b)&&xb(A({},a,{type:"mouseenter",bubbles:!1,cancelable:!1})),xb(A({},a,{type:"mouseover"})));break;case"_mouseout":Vb.blur(),Z.bubbleEvents===!0&&f&&(b&&b!==a.relatedTarget&&!F(a.relatedTarget,b)&&xb(A({},a,{type:"mouseleave",bubbles:!1,cancelable:!1})),xb(A({},a,{type:"mouseout"})));break;case"_mousedown":Kb(b,Z.activeClass),Z.bubbleEvents===!0&&f&&xb(A({},a,{type:a.type.slice(1)}));break;case"_mouseup":Lb(b,Z.activeClass),Z.bubbleEvents===!0&&f&&xb(A({},a,{type:a.type.slice(1)}));break;case"_click":e=null,Z.bubbleEvents===!0&&f&&xb(A({},a,{type:a.type.slice(1)}));break;case"_mousemove":Z.bubbleEvents===!0&&f&&xb(A({},a,{type:a.type.slice(1)}))}return/^_(?:click|mouse(?:over|out|down|up|move))$/.test(a.type)?!0:void 0},wb=function(a){if(a.errors&&a.errors.length>0){var b=B(a);A(b,{type:"error",name:"clipboard-error"}),delete b.success,i(function(){Vb.emit(b)},0)}},xb=function(a){if(a&&"string"==typeof a.type&&a){var b,c=a.target||null,d=c&&c.ownerDocument||g,e={view:d.defaultView||f,canBubble:!0,cancelable:!0,detail:"click"===a.type?1:0,button:"number"==typeof a.which?a.which-1:"number"==typeof a.button?a.button:d.createEvent?0:1},h=A(e,a);c&&d.createEvent&&c.dispatchEvent&&(h=[h.type,h.canBubble,h.cancelable,h.view,h.detail,h.screenX,h.screenY,h.clientX,h.clientY,h.ctrlKey,h.altKey,h.shiftKey,h.metaKey,h.button,h.relatedTarget],b=d.createEvent("MouseEvents"),b.initMouseEvent&&(b.initMouseEvent.apply(b,h),b._source="js",c.dispatchEvent(b)))}},yb=function(){var a=Z.flashLoadTimeout;if("number"==typeof a&&a>=0){var b=Math.min(1e3,a/10),c=Z.swfObjectId+"_fallbackContent";T=k(function(){var a=g.getElementById(c);Ob(a)&&(Pb(),N.deactivated=null,Vb.emit({type:"error",name:"swf-not-found"}))},b)}},zb=function(){var a=g.createElement("div");return a.id=Z.containerId,a.className=Z.containerClass,a.style.position="absolute",a.style.left="0px",a.style.top="-9999px",a.style.width="1px",a.style.height="1px",a.style.zIndex=""+Sb(Z.zIndex),a},Ab=function(a){for(var b=a&&a.parentNode;b&&"OBJECT"===b.nodeName&&b.parentNode;)b=b.parentNode;return b||null},Bb=function(){var a,b=N.bridge,c=Ab(b);if(!b){var d=Ib(f.location.host,Z),e="never"===d?"none":"all",h=Gb(A({jsVersion:Vb.version},Z)),i=Z.swfPath+Fb(Z.swfPath,Z);c=zb();var j=g.createElement("div");c.appendChild(j),g.body.appendChild(c);var k=g.createElement("div"),l="activex"===N.pluginType;k.innerHTML='"+(l?'':"")+'
 
',b=k.firstChild,k=null,y(b).ZeroClipboard=Vb,c.replaceChild(b,j),yb()}return b||(b=g[Z.swfObjectId],b&&(a=b.length)&&(b=b[a-1]),!b&&c&&(b=c.firstChild)),N.bridge=b||null,b},Cb=function(){var a=N.bridge;if(a){var d=Ab(a);d&&("activex"===N.pluginType&&"readyState"in a?(a.style.display="none",function e(){if(4===a.readyState){for(var b in a)"function"==typeof a[b]&&(a[b]=null);a.parentNode&&a.parentNode.removeChild(a),d.parentNode&&d.parentNode.removeChild(d)}else i(e,10)}()):(a.parentNode&&a.parentNode.removeChild(a),d.parentNode&&d.parentNode.removeChild(d))),Pb(),N.ready=null,N.bridge=null,N.deactivated=null,c=b}},Db=function(a){var b={},c={};if("object"==typeof a&&a){for(var d in a)if(d&&w.call(a,d)&&"string"==typeof a[d]&&a[d])switch(d.toLowerCase()){case"text/plain":case"text":case"air:text":case"flash:text":b.text=a[d],c.text=d;break;case"text/html":case"html":case"air:html":case"flash:html":b.html=a[d],c.html=d;break;case"application/rtf":case"text/rtf":case"rtf":case"richtext":case"air:rtf":case"flash:rtf":b.rtf=a[d],c.rtf=d}return{data:b,formatMap:c}}},Eb=function(a,b){if("object"!=typeof a||!a||"object"!=typeof b||!b)return a;var c={};for(var d in a)if(w.call(a,d))if("errors"===d){c[d]=a[d]?a[d].slice():[];for(var e=0,f=c[d].length;f>e;e++)c[d][e].format=b[c[d][e].format]}else if("success"!==d&&"data"!==d)c[d]=a[d];else{c[d]={};var g=a[d];for(var h in g)h&&w.call(g,h)&&w.call(b,h)&&(c[d][b[h]]=g[h])}return c},Fb=function(a,b){var c=null==b||b&&b.cacheBust===!0;return c?(-1===a.indexOf("?")?"?":"&")+"noCache="+t():""},Gb=function(a){var b,c,d,e,g="",h=[];if(a.trustedDomains&&("string"==typeof a.trustedDomains?e=[a.trustedDomains]:"object"==typeof a.trustedDomains&&"length"in a.trustedDomains&&(e=a.trustedDomains)),e&&e.length)for(b=0,c=e.length;c>b;b++)if(w.call(e,b)&&e[b]&&"string"==typeof e[b]){if(d=Hb(e[b]),!d)continue;if("*"===d){h.length=0,h.push(d);break}h.push.apply(h,[d,"//"+d,f.location.protocol+"//"+d])}return h.length&&(g+="trustedOrigins="+n(h.join(","))),a.forceEnhancedClipboard===!0&&(g+=(g?"&":"")+"forceEnhancedClipboard=true"),"string"==typeof a.swfObjectId&&a.swfObjectId&&(g+=(g?"&":"")+"swfObjectId="+n(a.swfObjectId)),"string"==typeof a.jsVersion&&a.jsVersion&&(g+=(g?"&":"")+"jsVersion="+n(a.jsVersion)),g},Hb=function(a){if(null==a||""===a)return null;if(a=a.replace(/^\s+|\s+$/g,""),""===a)return null;var b=a.indexOf("//");a=-1===b?a:a.slice(b+2);var c=a.indexOf("/");return a=-1===c?a:-1===b||0===c?null:a.slice(0,c),a&&".swf"===a.slice(-4).toLowerCase()?null:a||null},Ib=function(){var a=function(a){var b,c,d,e=[];if("string"==typeof a&&(a=[a]),"object"!=typeof a||!a||"number"!=typeof a.length)return e;for(b=0,c=a.length;c>b;b++)if(w.call(a,b)&&(d=Hb(a[b]))){if("*"===d){e.length=0,e.push("*");break}-1===e.indexOf(d)&&e.push(d)}return e};return function(b,c){var d=Hb(c.swfPath);null===d&&(d=b);var e=a(c.trustedDomains),f=e.length;if(f>0){if(1===f&&"*"===e[0])return"always";if(-1!==e.indexOf(b))return 1===f&&b===d?"sameDomain":"always"}return"never"}}(),Jb=function(){try{return g.activeElement}catch(a){return null}},Kb=function(a,b){var c,d,e,f=[];if("string"==typeof b&&b&&(f=b.split(/\s+/)),a&&1===a.nodeType&&f.length>0)if(a.classList)for(c=0,d=f.length;d>c;c++)a.classList.add(f[c]);else if(a.hasOwnProperty("className")){for(e=" "+a.className+" ",c=0,d=f.length;d>c;c++)-1===e.indexOf(" "+f[c]+" ")&&(e+=f[c]+" ");a.className=e.replace(/^\s+|\s+$/g,"")}return a},Lb=function(a,b){var c,d,e,f=[];if("string"==typeof b&&b&&(f=b.split(/\s+/)),a&&1===a.nodeType&&f.length>0)if(a.classList&&a.classList.length>0)for(c=0,d=f.length;d>c;c++)a.classList.remove(f[c]);else if(a.className){for(e=(" "+a.className+" ").replace(/[\r\n\t]/g," "),c=0,d=f.length;d>c;c++)e=e.replace(" "+f[c]+" "," ");a.className=e.replace(/^\s+|\s+$/g,"")}return a},Mb=function(a,b){var c=m(a,null).getPropertyValue(b);return"cursor"!==b||c&&"auto"!==c||"A"!==a.nodeName?c:"pointer"},Nb=function(a){var b={left:0,top:0,width:0,height:0};if(a.getBoundingClientRect){var c=a.getBoundingClientRect(),d=f.pageXOffset,e=f.pageYOffset,h=g.documentElement.clientLeft||0,i=g.documentElement.clientTop||0,j=0,k=0;if("relative"===Mb(g.body,"position")){var l=g.body.getBoundingClientRect(),m=g.documentElement.getBoundingClientRect();j=l.left-m.left||0,k=l.top-m.top||0}b.left=c.left+d-h-j,b.top=c.top+e-i-k,b.width="width"in c?c.width:c.right-c.left,b.height="height"in c?c.height:c.bottom-c.top}return b},Ob=function(a){if(!a)return!1;var b=m(a,null),c=r(b.height)>0,d=r(b.width)>0,e=r(b.top)>=0,f=r(b.left)>=0,g=c&&d&&e&&f,h=g?null:Nb(a),i="none"!==b.display&&"collapse"!==b.visibility&&(g||!!h&&(c||h.height>0)&&(d||h.width>0)&&(e||h.top>=0)&&(f||h.left>=0));return i},Pb=function(){j(S),S=0,l(T),T=0},Qb=function(){var a;if(d&&(a=Ab(N.bridge))){var b=Nb(d);A(a.style,{width:b.width+"px",height:b.height+"px",top:b.top+"px",left:b.left+"px",zIndex:""+Sb(Z.zIndex)})}},Rb=function(a){N.ready===!0&&(N.bridge&&"function"==typeof N.bridge.setHandCursor?N.bridge.setHandCursor(a):N.ready=!1)},Sb=function(a){if(/^(?:auto|inherit)$/.test(a))return a;var b;return"number"!=typeof a||s(a)?"string"==typeof a&&(b=Sb(q(a,10))):b=a,"number"==typeof b?b:"auto"},Tb=function(b){var c,d,e,f=N.sandboxed,g=null;if(b=b===!0,M===!1)g=!1;else{try{d=a.frameElement||null}catch(h){e={name:h.name,message:h.message}}if(d&&1===d.nodeType&&"IFRAME"===d.nodeName)try{g=d.hasAttribute("sandbox")}catch(h){g=null}else{try{c=document.domain||null}catch(h){c=null}(null===c||e&&"SecurityError"===e.name&&/(^|[\s\(\[@])sandbox(es|ed|ing|[\s\.,!\)\]@]|$)/.test(e.message.toLowerCase()))&&(g=!0)}}return N.sandboxed=g,f===g||b||Ub(o),g},Ub=function(a){function b(a){var b=a.match(/[\d]+/g);return b.length=3,b.join(".")}function c(a){return!!a&&(a=a.toLowerCase())&&(/^(pepflashplayer\.dll|libpepflashplayer\.so|pepperflashplayer\.plugin)$/.test(a)||"chrome.plugin"===a.slice(-13))}function d(a){a&&(i=!0,a.version&&(l=b(a.version)),!l&&a.description&&(l=b(a.description)),a.filename&&(k=c(a.filename)))}var e,f,g,i=!1,j=!1,k=!1,l="";if(h.plugins&&h.plugins.length)e=h.plugins["Shockwave Flash"],d(e),h.plugins["Shockwave Flash 2.0"]&&(i=!0,l="2.0.0.11");else if(h.mimeTypes&&h.mimeTypes.length)g=h.mimeTypes["application/x-shockwave-flash"],e=g&&g.enabledPlugin,d(e);else if("undefined"!=typeof a){j=!0;try{f=new a("ShockwaveFlash.ShockwaveFlash.7"),i=!0,l=b(f.GetVariable("$version"))}catch(m){try{f=new a("ShockwaveFlash.ShockwaveFlash.6"),i=!0,l="6.0.21"}catch(n){try{f=new a("ShockwaveFlash.ShockwaveFlash"),i=!0,l=b(f.GetVariable("$version"))}catch(o){j=!1}}}}N.disabled=i!==!0,N.outdated=l&&r(l)e;e++)a=g[e].replace(/^on/,""),h[a]=!0,j[a]||(j[a]=[]),j[a].push(d);if(h.ready&&N.ready&&this.emit({type:"ready",client:this}),h.error){for(e=0,f=W.length;f>e;e++)if(N[W[e].replace(/^flash-/,"")]){this.emit({type:"error",name:W[e],client:this});break}c!==b&&Vb.version!==c&&this.emit({type:"error",name:"version-mismatch",jsVersion:Vb.version,swfVersion:c})}}return this},bc=function(a,b){var c,d,e,f,g,h=Xb[this.id],i=h&&h.handlers;if(!i)return this;if(0===arguments.length)f=u(i);else if("string"==typeof a&&a)f=a.split(/\s+/);else if("object"==typeof a&&a&&"undefined"==typeof b)for(c in a)w.call(a,c)&&"string"==typeof c&&c&&"function"==typeof a[c]&&this.off(c,a[c]);if(f&&f.length)for(c=0,d=f.length;d>c;c++)if(a=f[c].toLowerCase().replace(/^on/,""),g=i[a],g&&g.length)if(b)for(e=g.indexOf(b);-1!==e;)g.splice(e,1),e=g.indexOf(b,e);else g.length=0;return this},cc=function(a){var b=null,c=Xb[this.id]&&Xb[this.id].handlers;return c&&(b="string"==typeof a&&a?c[a]?c[a].slice(0):[]:B(c)),b},dc=function(a){if(ic.call(this,a)){"object"==typeof a&&a&&"string"==typeof a.type&&a.type&&(a=A({},a));var b=A({},ob(a),{client:this});jc.call(this,b)}return this},ec=function(a){if(!Xb[this.id])throw new Error("Attempted to clip element(s) to a destroyed ZeroClipboard client instance");a=kc(a);for(var b=0;b0,e=!a.target||d&&-1!==c.indexOf(a.target),f=a.relatedTarget&&d&&-1!==c.indexOf(a.relatedTarget),g=a.client&&a.client===this;return b&&(e||f||g)?!0:!1},jc=function(a){var b=Xb[this.id];if("object"==typeof a&&a&&a.type&&b){var c=rb(a),d=b&&b.handlers["*"]||[],e=b&&b.handlers[a.type]||[],g=d.concat(e);if(g&&g.length){var h,i,j,k,l,m=this;for(h=0,i=g.length;i>h;h++)j=g[h],k=m,"string"==typeof j&&"function"==typeof f[j]&&(j=f[j]),"object"==typeof j&&j&&"function"==typeof j.handleEvent&&(k=j,j=j.handleEvent),"function"==typeof j&&(l=A({},a),sb(j,k,[l],c))}}},kc=function(a){return"string"==typeof a&&(a=[]),"number"!=typeof a.length?[a]:a},lc=function(a){if(a&&1===a.nodeType){var b=function(a){(a||(a=f.event))&&("js"!==a._source&&(a.stopImmediatePropagation(),a.preventDefault()),delete a._source)},c=function(c){(c||(c=f.event))&&(b(c),Vb.focus(a))};a.addEventListener("mouseover",c,!1),a.addEventListener("mouseout",b,!1),a.addEventListener("mouseenter",b,!1),a.addEventListener("mouseleave",b,!1),a.addEventListener("mousemove",b,!1),$b[a.zcClippingId]={mouseover:c,mouseout:b,mouseenter:b,mouseleave:b,mousemove:b}}},mc=function(a){if(a&&1===a.nodeType){var b=$b[a.zcClippingId];if("object"==typeof b&&b){for(var c,d,e=["move","leave","enter","out","over"],f=0,g=e.length;g>f;f++)c="mouse"+e[f],d=b[c],"function"==typeof d&&a.removeEventListener(c,d,!1);delete $b[a.zcClippingId]}}};Vb._createClient=function(){_b.apply(this,z(arguments))},Vb.prototype.on=function(){return ac.apply(this,z(arguments))},Vb.prototype.off=function(){return bc.apply(this,z(arguments))},Vb.prototype.handlers=function(){return cc.apply(this,z(arguments))},Vb.prototype.emit=function(){return dc.apply(this,z(arguments))},Vb.prototype.clip=function(){return ec.apply(this,z(arguments))},Vb.prototype.unclip=function(){return fc.apply(this,z(arguments))},Vb.prototype.elements=function(){return gc.apply(this,z(arguments))},Vb.prototype.destroy=function(){return hc.apply(this,z(arguments))},Vb.prototype.setText=function(a){if(!Xb[this.id])throw new Error("Attempted to set pending clipboard data from a destroyed ZeroClipboard client instance");return Vb.setData("text/plain",a),this},Vb.prototype.setHtml=function(a){if(!Xb[this.id])throw new Error("Attempted to set pending clipboard data from a destroyed ZeroClipboard client instance");return Vb.setData("text/html",a),this},Vb.prototype.setRichText=function(a){if(!Xb[this.id])throw new Error("Attempted to set pending clipboard data from a destroyed ZeroClipboard client instance");return Vb.setData("application/rtf",a),this},Vb.prototype.setData=function(){if(!Xb[this.id])throw new Error("Attempted to set pending clipboard data from a destroyed ZeroClipboard client instance");return Vb.setData.apply(this,z(arguments)),this},Vb.prototype.clearData=function(){if(!Xb[this.id])throw new Error("Attempted to clear pending clipboard data from a destroyed ZeroClipboard client instance");return Vb.clearData.apply(this,z(arguments)),this},Vb.prototype.getData=function(){if(!Xb[this.id])throw new Error("Attempted to get pending clipboard data from a destroyed ZeroClipboard client instance");return Vb.getData.apply(this,z(arguments))},"function"==typeof define&&define.amd?define(function(){return Vb}):"object"==typeof module&&module&&"object"==typeof module.exports&&module.exports?module.exports=Vb:a.ZeroClipboard=Vb}(function(){return this||window}()); +//# sourceMappingURL=ZeroClipboard.min.map \ No newline at end of file diff --git a/bower_components/zeroclipboard/dist/ZeroClipboard.min.map b/bower_components/zeroclipboard/dist/ZeroClipboard.min.map new file mode 100644 index 0000000..8d7dc47 --- /dev/null +++ b/bower_components/zeroclipboard/dist/ZeroClipboard.min.map @@ -0,0 +1 @@ +{"version":3,"file":"ZeroClipboard.min.js","sources":["ZeroClipboard.js"],"names":["window","undefined","_zcSwfVersion","_currentElement","_copyTarget","_window","_document","document","_navigator","navigator","_setTimeout","setTimeout","_clearTimeout","clearTimeout","_setInterval","setInterval","_clearInterval","clearInterval","_getComputedStyle","getComputedStyle","_encodeURIComponent","encodeURIComponent","_ActiveXObject","ActiveXObject","_Error","Error","_parseInt","Number","parseInt","_parseFloat","parseFloat","_isNaN","isNaN","_now","Date","now","_keys","Object","keys","_defineProperty","defineProperty","_hasOwn","prototype","hasOwnProperty","_slice","Array","slice","_unwrap","unwrapper","el","wrap","unwrap","div","createElement","unwrappedDiv","nodeType","e","_args","argumentsObj","call","_extend","i","len","arg","prop","src","copy","args","arguments","target","length","_deepCopy","source","_pick","obj","newObj","_omit","indexOf","_deleteOwnProperties","_containedBy","ancestorEl","ownerDocument","parentNode","_getDirPathOfUrl","url","dir","split","lastIndexOf","_getCurrentScriptUrlFromErrorStack","stack","matches","match","_getCurrentScriptUrlFromError","err","sourceURL","fileName","_getCurrentScriptUrl","jsPath","scripts","currentScript","getElementsByTagName","readyState","_getUnanimousScriptParentDir","jsDir","_getDefaultSwfPath","_pageIsFramed","opener","top","parent","_flashState","bridge","version","pluginType","disabled","outdated","sandboxed","unavailable","degraded","deactivated","overdue","ready","_minimumFlashVersion","_handlers","_clipData","_clipDataFormatMap","_flashCheckTimeout","_swfFallbackCheckInterval","_eventMessages","error","flash-disabled","flash-outdated","flash-sandboxed","flash-unavailable","flash-degraded","flash-deactivated","flash-overdue","version-mismatch","clipboard-error","config-mismatch","swf-not-found","_errorsThatOnlyOccurAfterFlashLoads","_flashStateErrorNames","_flashStateErrorNameMatchingRegex","RegExp","map","errorName","replace","join","_flashStateEnabledErrorNameMatchingRegex","_globalConfig","swfPath","trustedDomains","location","host","cacheBust","forceEnhancedClipboard","flashLoadTimeout","autoActivate","bubbleEvents","containerId","containerClass","swfObjectId","hoverClass","activeClass","forceHandCursor","title","zIndex","_config","options","test","_isValidHtml4Id","_state","_detectSandbox","browser","flash","zeroclipboard","ZeroClipboard","config","_isFlashUnusable","_on","eventType","listener","events","added","toLowerCase","on","push","emit","type","name","jsVersion","swfVersion","_off","foundIndex","perEventHandlers","off","splice","_listeners","_emit","event","eventCopy","returnVal","tmp","_createEvent","_preprocessEvent","_dispatchCallbacks","this","_mapClipDataToFlash","data","formatMap","_create","previousState","isFlashUnusable","maxWait","_embedSwf","_destroy","clearData","blur","_unembedSwf","_setData","format","dataObj","dataFormat","_clearData","_getData","_focus","element","_removeClass","_addClass","newTitle","getAttribute","htmlBridge","_getHtmlBridge","setAttribute","useHandCursor","_getStyle","_setHandCursor","_reposition","_blur","removeAttribute","style","left","width","height","_activeElement","id","relatedTarget","currentTarget","timeStamp","msg","message","minimumVersion","clipboardData","setData","_mapClipResultsFromFlash","_getRelatedTarget","_addMouseData","targetEl","relatedTargetId","getElementById","srcElement","fromElement","toElement","pos","_getElementPosition","screenLeft","screenX","screenTop","screenY","scrollLeft","body","documentElement","scrollTop","pageX","_stageX","pageY","_stageY","clientX","clientY","moveX","movementX","moveY","movementY","x","y","offsetX","offsetY","layerX","layerY","_shouldPerformAsync","_dispatchCallback","func","context","async","apply","wildcardTypeHandlers","specificTypeHandlers","handlers","concat","originalContext","handleEvent","_getSandboxStatusFromErrorEvent","isSandboxed","sourceIsSwf","_source","_clearTimeoutsAndPolling","wasDeactivated","textContent","htmlContent","value","outerHTML","innerHTML","innerText","_queueEmitClipboardErrors","_safeActiveElement","focus","_fireMouseEvent","bubbles","cancelable","aftercopyEvent","errors","errorEvent","success","doc","defaults","view","defaultView","canBubble","detail","button","which","createEvent","dispatchEvent","ctrlKey","altKey","shiftKey","metaKey","initMouseEvent","_watchForSwfFallbackContent","pollWait","Math","min","fallbackContentId","_isElementVisible","_createHtmlBridge","container","className","position","_getSafeZIndex","flashBridge","nodeName","allowScriptAccess","_determineScriptAccess","allowNetworking","flashvars","_vars","swfUrl","_cacheBust","divToBeReplaced","appendChild","tmpDiv","usingActiveX","firstChild","replaceChild","display","removeSwfFromIE","removeChild","clipData","newClipData","text","html","rtf","clipResults","newResults","tmpHash","path","domain","domains","str","trustedOriginsExpanded","_extractDomain","protocol","originOrUrl","protocolIndex","pathIndex","_extractAllDomains","origins","resultsArray","currentDomain","configOptions","swfDomain","activeElement","c","cl","classNames","classList","add","remove","getPropertyValue","getBoundingClientRect","elRect","pageXOffset","pageYOffset","leftBorderWidth","clientLeft","topBorderWidth","clientTop","leftBodyOffset","topBodyOffset","bodyRect","htmlRect","right","bottom","styles","hasCssHeight","hasCssWidth","hasCssTop","hasCssLeft","cssKnows","rect","isVisible","visibility","enabled","setHandCursor","val","doNotReassessFlashSupport","effectiveScriptOrigin","frame","frameError","frameElement","hasAttribute","_detectFlashSupport","parseFlashVersion","desc","isPepperFlash","flashPlayerFileName","inspectPlugin","plugin","hasFlash","flashVersion","description","filename","isPPAPI","ax","mimeType","isActiveX","plugins","mimeTypes","enabledPlugin","GetVariable","e1","e2","e3","_createClient","writable","configurable","enumerable","state","create","destroy","getData","activate","deactivate","_clientIdCounter","_clientMeta","_elementIdCounter","_elementMeta","_mouseHandlers","_clientConstructor","elements","client","instance","clip","_clientOn","meta","_clientOff","_clientListeners","_clientEmit","_clientShouldEmit","_clientDispatchCallbacks","_clientClip","_prepClip","zcClippingId","_addMouseHandlers","clippedElements","_clientUnclip","arrayIndex","clientIds","_removeMouseHandlers","_clientElements","_clientDestroy","unclip","clippedEls","hasClippedEls","goodTarget","goodRelTarget","goodClient","_suppressMouseEvents","stopImmediatePropagation","preventDefault","_elementMouseOver","addEventListener","mouseover","mouseout","mouseenter","mouseleave","mousemove","mouseHandlers","key","mouseEvents","removeEventListener","setText","setHtml","setRichText","richText","define","amd","module","exports"],"mappings":";;;;;;;;CAQA,SAAUA,EAAQC,GAChB,YAKA,IAoSIC,GAUAC,EAKAC,EAnTAC,EAAUL,EAAQM,EAAYD,EAAQE,SAAUC,EAAaH,EAAQI,UAAWC,EAAcL,EAAQM,WAAYC,EAAgBP,EAAQQ,aAAcC,EAAeT,EAAQU,YAAaC,EAAiBX,EAAQY,cAAeC,EAAoBb,EAAQc,iBAAkBC,EAAsBf,EAAQgB,mBAAoBC,EAAiBjB,EAAQkB,cAAeC,EAASnB,EAAQoB,MAAOC,EAAYrB,EAAQsB,OAAOC,UAAYvB,EAAQuB,SAAUC,EAAcxB,EAAQsB,OAAOG,YAAczB,EAAQyB,WAAYC,EAAS1B,EAAQsB,OAAOK,OAAS3B,EAAQ2B,MAAOC,EAAO5B,EAAQ6B,KAAKC,IAAKC,EAAQ/B,EAAQgC,OAAOC,KAAMC,EAAkBlC,EAAQgC,OAAOG,eAAgBC,EAAUpC,EAAQgC,OAAOK,UAAUC,eAAgBC,EAASvC,EAAQwC,MAAMH,UAAUI,MAAOC,EAAU,WAC1vB,GAAIC,GAAY,SAASC,GACvB,MAAOA,GAET,IAA4B,kBAAjB5C,GAAQ6C,MAAiD,kBAAnB7C,GAAQ8C,OACvD,IACE,GAAIC,GAAM9C,EAAU+C,cAAc,OAC9BC,EAAejD,EAAQ8C,OAAOC,EACb,KAAjBA,EAAIG,UAAkBD,GAA0C,IAA1BA,EAAaC,WACrDP,EAAY3C,EAAQ8C,QAEtB,MAAOK,IAEX,MAAOR,MAQLS,EAAQ,SAASC,GACnB,MAAOd,GAAOe,KAAKD,EAAc,IAQ/BE,EAAU,WACZ,GAAIC,GAAGC,EAAKC,EAAKC,EAAMC,EAAKC,EAAMC,EAAOV,EAAMW,WAAYC,EAASF,EAAK,MACzE,KAAKN,EAAI,EAAGC,EAAMK,EAAKG,OAAYR,EAAJD,EAASA,IACtC,GAAuB,OAAlBE,EAAMI,EAAKN,IACd,IAAKG,IAAQD,GACPtB,EAAQkB,KAAKI,EAAKC,KACpBC,EAAMI,EAAOL,GACbE,EAAOH,EAAIC,GACPK,IAAWH,GAAQA,IAASjE,IAC9BoE,EAAOL,GAAQE,GAMzB,OAAOG,IAQLE,EAAY,SAASC,GACvB,GAAIN,GAAML,EAAGC,EAAKE,CAClB,IAAsB,gBAAXQ,IAAiC,MAAVA,GAA6C,gBAApBA,GAAOjB,SAChEW,EAAOM,MACF,IAA6B,gBAAlBA,GAAOF,OAEvB,IADAJ,KACKL,EAAI,EAAGC,EAAMU,EAAOF,OAAYR,EAAJD,EAASA,IACpCpB,EAAQkB,KAAKa,EAAQX,KACvBK,EAAKL,GAAKU,EAAUC,EAAOX,SAG1B,CACLK,IACA,KAAKF,IAAQQ,GACP/B,EAAQkB,KAAKa,EAAQR,KACvBE,EAAKF,GAAQO,EAAUC,EAAOR,KAIpC,MAAOE,IAULO,EAAQ,SAASC,EAAKpC,GAExB,IAAK,GADDqC,MACKd,EAAI,EAAGC,EAAMxB,EAAKgC,OAAYR,EAAJD,EAASA,IACtCvB,EAAKuB,IAAMa,KACbC,EAAOrC,EAAKuB,IAAMa,EAAIpC,EAAKuB,IAG/B,OAAOc,IASLC,EAAQ,SAASF,EAAKpC,GACxB,GAAIqC,KACJ,KAAK,GAAIX,KAAQU,GACY,KAAvBpC,EAAKuC,QAAQb,KACfW,EAAOX,GAAQU,EAAIV,GAGvB,OAAOW,IAQLG,EAAuB,SAASJ,GAClC,GAAIA,EACF,IAAK,GAAIV,KAAQU,GACXjC,EAAQkB,KAAKe,EAAKV,UACbU,GAAIV,EAIjB,OAAOU,IAQLK,EAAe,SAAS9B,EAAI+B,GAC9B,GAAI/B,GAAsB,IAAhBA,EAAGM,UAAkBN,EAAGgC,eAAiBD,IAAuC,IAAxBA,EAAWzB,UAAkByB,EAAWC,eAAiBD,EAAWC,gBAAkBhC,EAAGgC,eAAyC,IAAxBD,EAAWzB,WAAmByB,EAAWC,eAAiBD,IAAe/B,EAAGgC,eACtP,EAAG,CACD,GAAIhC,IAAO+B,EACT,OAAO,CAET/B,GAAKA,EAAGiC,iBACDjC,EAEX,QAAO,GAQLkC,EAAmB,SAASC,GAC9B,GAAIC,EAKJ,OAJmB,gBAARD,IAAoBA,IAC7BC,EAAMD,EAAIE,MAAM,KAAK,GAAGA,MAAM,KAAK,GACnCD,EAAMD,EAAItC,MAAM,EAAGsC,EAAIG,YAAY,KAAO,IAErCF,GAQLG,EAAqC,SAASC,GAChD,GAAIL,GAAKM,CAYT,OAXqB,gBAAVD,IAAsBA,IAC/BC,EAAUD,EAAME,MAAM,sIAClBD,GAAWA,EAAQ,GACrBN,EAAMM,EAAQ,IAEdA,EAAUD,EAAME,MAAM,kEAClBD,GAAWA,EAAQ,KACrBN,EAAMM,EAAQ,MAIbN,GAQLQ,EAAgC,WAClC,GAAIR,GAAKS,CACT,KACE,KAAM,IAAIrE,GACV,MAAOgC,GACPqC,EAAMrC,EAKR,MAHIqC,KACFT,EAAMS,EAAIC,WAAaD,EAAIE,UAAYP,EAAmCK,EAAIJ,QAEzEL,GAQLY,EAAuB,WACzB,GAAIC,GAAQC,EAASrC,CACrB,IAAIvD,EAAU6F,gBAAkBF,EAAS3F,EAAU6F,cAAclC,KAC/D,MAAOgC,EAGT,IADAC,EAAU5F,EAAU8F,qBAAqB,UAClB,IAAnBF,EAAQ5B,OACV,MAAO4B,GAAQ,GAAGjC,KAAOhE,CAE3B,IAAI,cAAgBiG,GAAQ,GAC1B,IAAKrC,EAAIqC,EAAQ5B,OAAQT,KACvB,GAA8B,gBAA1BqC,EAAQrC,GAAGwC,aAAiCJ,EAASC,EAAQrC,GAAGI,KAClE,MAAOgC,EAIb,OAA6B,YAAzB3F,EAAU+F,aAA6BJ,EAASC,EAAQA,EAAQ5B,OAAS,GAAGL,KACvEgC,GAELA,EAASL,KACJK,EAEFhG,GAULqG,EAA+B,WACjC,GAAIzC,GAAG0C,EAAON,EAAQC,EAAU5F,EAAU8F,qBAAqB,SAC/D,KAAKvC,EAAIqC,EAAQ5B,OAAQT,KAAO,CAC9B,KAAMoC,EAASC,EAAQrC,GAAGI,KAAM,CAC9BsC,EAAQ,IACR,OAGF,GADAN,EAASd,EAAiBc,GACb,MAATM,EACFA,EAAQN,MACH,IAAIM,IAAUN,EAAQ,CAC3BM,EAAQ,IACR,QAGJ,MAAOA,IAAStG,GASduG,EAAqB,WACvB,GAAID,GAAQpB,EAAiBa,MAA2BM,KAAkC,EAC1F,OAAOC,GAAQ,qBAMbE,EAAgB,WAClB,MAAwB,OAAjBzG,EAAO0G,WAAqB1G,EAAO2G,KAAO3G,GAAUA,EAAO2G,OAAS3G,EAAO4G,QAAU5G,GAAUA,EAAO4G,WAM3GC,GACFC,OAAQ,KACRC,QAAS,QACTC,WAAY,UACZC,SAAU,KACVC,SAAU,KACVC,UAAW,KACXC,YAAa,KACbC,SAAU,KACVC,YAAa,KACbC,QAAS,KACTC,MAAO,MAOLC,EAAuB,SASvBC,KAeAC,KAKAC,EAAqB,KAKrBC,EAAqB,EAKrBC,EAA4B,EAK5BC,GACFP,MAAO,qCACPQ,OACEC,iBAAkB,sHAClBC,iBAAkB,iDAClBC,kBAAmB,qEACnBC,oBAAqB,iEACrBC,iBAAkB,+EAClBC,oBAAqB,0TACrBC,gBAAiB,+EACjBC,mBAAoB,kFACpBC,kBAAmB,0GACnBC,kBAAmB,6DACnBC,gBAAiB,+HAQjBC,GAAwC,oBAAqB,iBAAkB,gBAAiB,mBAAoB,kBAAmB,mBAMvIC,GAA0B,iBAAkB,iBAAkB,kBAAmB,oBAAqB,iBAAkB,oBAAqB,iBAK7IC,EAAoC,GAAIC,QAAO,WAAaF,EAAsBG,IAAI,SAASC,GACjG,MAAOA,GAAUC,QAAQ,UAAW,MACnCC,KAAK,KAAO,MAMXC,EAA2C,GAAIL,QAAO,WAAaF,EAAsB/F,MAAM,GAAGkG,IAAI,SAASC,GACjH,MAAOA,GAAUC,QAAQ,UAAW,MACnCC,KAAK,KAAO,MAKXE,GACFC,QAAS9C,IACT+C,eAAgBvJ,EAAOwJ,SAASC,MAASzJ,EAAOwJ,SAASC,SACzDC,WAAW,EACXC,wBAAwB,EACxBC,iBAAkB,IAClBC,cAAc,EACdC,cAAc,EACdC,YAAa,mCACbC,eAAgB,iCAChBC,YAAa,oCACbC,WAAY,yBACZC,YAAa,0BACbC,iBAAiB,EACjBC,MAAO,KACPC,OAAQ,WAMNC,EAAU,SAASC,GACrB,GAAuB,gBAAZA,IAAoC,OAAZA,EACjC,IAAK,GAAIxG,KAAQwG,GACf,GAAI/H,EAAQkB,KAAK6G,EAASxG,GACxB,GAAI,kDAAkDyG,KAAKzG,GACzDqF,EAAcrF,GAAQwG,EAAQxG,OACzB,IAA0B,MAAtB6C,EAAYC,OACrB,GAAa,gBAAT9C,GAAmC,gBAATA,EAAwB,CACpD,IAAI0G,GAAgBF,EAAQxG,IAG1B,KAAM,IAAIvC,OAAM,kBAAoBuC,EAAO,8CAF3CqF,GAAcrF,GAAQwG,EAAQxG,OAKhCqF,GAAcrF,GAAQwG,EAAQxG,EAMxC,EAAA,GAAuB,gBAAZwG,KAAwBA,EAMnC,MAAOjG,GAAU8E,EALf,IAAI5G,EAAQkB,KAAK0F,EAAemB,GAC9B,MAAOnB,GAAcmB,KAUvBG,EAAS,WAEX,MADAC,OAEEC,QAASpG,EAAMjE,GAAc,YAAa,WAAY,YACtDsK,MAAOlG,EAAMiC,GAAe,WAC5BkE,eACEhE,QAASiE,GAAcjE,QACvBkE,OAAQD,GAAcC,YAQxBC,GAAmB,WACrB,SAAUrE,EAAYI,UAAYJ,EAAYK,UAAYL,EAAYM,WAAaN,EAAYO,aAAeP,EAAYQ,UAAYR,EAAYS,cAMhJ6D,GAAM,SAASC,EAAWC,GAC5B,GAAIxH,GAAGC,EAAKwH,EAAQC,IACpB,IAAyB,gBAAdH,IAA0BA,EACnCE,EAASF,EAAUI,cAAclG,MAAM,WAClC,IAAyB,gBAAd8F,IAA0BA,GAAiC,mBAAbC,GAC9D,IAAKxH,IAAKuH,GACJ3I,EAAQkB,KAAKyH,EAAWvH,IAAmB,gBAANA,IAAkBA,GAA6B,kBAAjBuH,GAAUvH,IAC/EmH,GAAcS,GAAG5H,EAAGuH,EAAUvH,GAIpC,IAAIyH,GAAUA,EAAOhH,OAAQ,CAC3B,IAAKT,EAAI,EAAGC,EAAMwH,EAAOhH,OAAYR,EAAJD,EAASA,IACxCuH,EAAYE,EAAOzH,GAAGqF,QAAQ,MAAO,IACrCqC,EAAMH,IAAa,EACd1D,EAAU0D,KACb1D,EAAU0D,OAEZ1D,EAAU0D,GAAWM,KAAKL,EAO5B,IALIE,EAAM/D,OAASX,EAAYW,OAC7BwD,GAAcW,MACZC,KAAM,UAGNL,EAAMvD,MAAO,CACf,IAAKnE,EAAI,EAAGC,EAAM+E,EAAsBvE,OAAYR,EAAJD,EAASA,IACvD,GAAIgD,EAAYgC,EAAsBhF,GAAGqF,QAAQ,UAAW,QAAS,EAAM,CACzE8B,GAAcW,MACZC,KAAM,QACNC,KAAMhD,EAAsBhF,IAE9B,OAGA3D,IAAkBD,GAAa+K,GAAcjE,UAAY7G,GAC3D8K,GAAcW,MACZC,KAAM,QACNC,KAAM,mBACNC,UAAWd,GAAcjE,QACzBgF,WAAY7L,KAKpB,MAAO8K,KAMLgB,GAAO,SAASZ,EAAWC,GAC7B,GAAIxH,GAAGC,EAAKmI,EAAYX,EAAQY,CAChC,IAAyB,IAArB9H,UAAUE,OACZgH,EAASlJ,EAAMsF,OACV,IAAyB,gBAAd0D,IAA0BA,EAC1CE,EAASF,EAAU9F,MAAM,WACpB,IAAyB,gBAAd8F,IAA0BA,GAAiC,mBAAbC,GAC9D,IAAKxH,IAAKuH,GACJ3I,EAAQkB,KAAKyH,EAAWvH,IAAmB,gBAANA,IAAkBA,GAA6B,kBAAjBuH,GAAUvH,IAC/EmH,GAAcmB,IAAItI,EAAGuH,EAAUvH,GAIrC,IAAIyH,GAAUA,EAAOhH,OACnB,IAAKT,EAAI,EAAGC,EAAMwH,EAAOhH,OAAYR,EAAJD,EAASA,IAGxC,GAFAuH,EAAYE,EAAOzH,GAAG2H,cAActC,QAAQ,MAAO,IACnDgD,EAAmBxE,EAAU0D,GACzBc,GAAoBA,EAAiB5H,OACvC,GAAI+G,EAEF,IADAY,EAAaC,EAAiBrH,QAAQwG,GAChB,KAAfY,GACLC,EAAiBE,OAAOH,EAAY,GACpCA,EAAaC,EAAiBrH,QAAQwG,EAAUY,OAGlDC,GAAiB5H,OAAS,CAKlC,OAAO0G,KAMLqB,GAAa,SAASjB,GACxB,GAAIlH,EAMJ,OAJEA,GADuB,gBAAdkH,IAA0BA,EAC5B7G,EAAUmD,EAAU0D,KAAe,KAEnC7G,EAAUmD,IAQjB4E,GAAQ,SAASC,GACnB,GAAIC,GAAWC,EAAWC,CAE1B,OADAH,GAAQI,GAAaJ,GAChBA,IAGDK,GAAiBL,GAGF,UAAfA,EAAMX,MAAoB/E,EAAYU,WAAY,EAC7CyD,GAAcW,MACnBC,KAAM,QACNC,KAAM,mBAGVW,EAAY5I,KAAY2I,GACxBM,GAAmBlJ,KAAKmJ,KAAMN,GACX,SAAfD,EAAMX,OACRc,EAAMK,GAAoBpF,GAC1B8E,EAAYC,EAAIM,KAChBpF,EAAqB8E,EAAIO,WAEpBR,GAnBP,QAyBES,GAAU,WACZ,GAAIC,GAAgBtG,EAAYM,SAKhC,IAJAyD,KACiC,iBAAtB/D,GAAYW,QACrBX,EAAYW,OAAQ,GAElBX,EAAYM,YAAcgG,GAAiBtG,EAAYM,aAAc,EACvEN,EAAYW,OAAQ,EACpBwD,GAAcW,MACZC,KAAM,QACNC,KAAM,wBAEH,KAAKb,GAAcoC,mBAA4C,OAAvBvG,EAAYC,OAAiB,CAC1E,GAAIuG,GAAUhE,EAAcO,gBACL,iBAAZyD,IAAwBA,GAAW,IAC5CxF,EAAqBnH,EAAY,WACQ,iBAA5BmG,GAAYS,cACrBT,EAAYS,aAAc,GAExBT,EAAYS,eAAgB,GAC9B0D,GAAcW,MACZC,KAAM,QACNC,KAAM,uBAGTwB,IAELxG,EAAYU,SAAU,EACtB+F,OAOAC,GAAW,WACbvC,GAAcwC,YACdxC,GAAcyC,OACdzC,GAAcW,KAAK,WACnB+B,KACA1C,GAAcmB,OAMZwB,GAAW,SAASC,EAAQZ,GAC9B,GAAIa,EACJ,IAAsB,gBAAXD,IAAuBA,GAA0B,mBAATZ,GACjDa,EAAUD,EACV5C,GAAcwC,gBACT,CAAA,GAAsB,gBAAXI,KAAuBA,EAIvC,MAHAC,MACAA,EAAQD,GAAUZ,EAIpB,IAAK,GAAIc,KAAcD,GACK,gBAAfC,IAA2BA,GAAcrL,EAAQkB,KAAKkK,EAASC,IAA8C,gBAAxBD,GAAQC,IAA4BD,EAAQC,KAC1InG,EAAUmG,GAAcD,EAAQC,KAQlCC,GAAa,SAASH,GACF,mBAAXA,IACT9I,EAAqB6C,GACrBC,EAAqB,MACM,gBAAXgG,IAAuBnL,EAAQkB,KAAKgE,EAAWiG,UACxDjG,GAAUiG,IAOjBI,GAAW,SAASJ,GACtB,MAAsB,mBAAXA,GACFrJ,EAAUoD,GACU,gBAAXiG,IAAuBnL,EAAQkB,KAAKgE,EAAWiG,GACxDjG,EAAUiG,GADZ,QAQLK,GAAS,SAASC,GACpB,GAAMA,GAAgC,IAArBA,EAAQ3K,SAAzB,CAGIpD,IACFgO,GAAahO,EAAiBkJ,EAAcc,aACxChK,IAAoB+N,GACtBC,GAAahO,EAAiBkJ,EAAca,aAGhD/J,EAAkB+N,EAClBE,GAAUF,EAAS7E,EAAca,WACjC,IAAImE,GAAWH,EAAQI,aAAa,UAAYjF,EAAcgB,KAC9D,IAAwB,gBAAbgE,IAAyBA,EAAU,CAC5C,GAAIE,GAAaC,GAAe3H,EAAYC,OACxCyH,IACFA,EAAWE,aAAa,QAASJ,GAGrC,GAAIK,GAAgBrF,EAAce,mBAAoB,GAAyC,YAAjCuE,GAAUT,EAAS,SACjFU,IAAeF,GACfG,OAMEC,GAAQ,WACV,GAAIP,GAAaC,GAAe3H,EAAYC,OACxCyH,KACFA,EAAWQ,gBAAgB,SAC3BR,EAAWS,MAAMC,KAAO,MACxBV,EAAWS,MAAMrI,IAAM,UACvB4H,EAAWS,MAAME,MAAQ,MACzBX,EAAWS,MAAMG,OAAS,OAExBhP,IACFgO,GAAahO,EAAiBkJ,EAAca,YAC5CiE,GAAahO,EAAiBkJ,EAAcc,aAC5ChK,EAAkB,OAOlBiP,GAAiB,WACnB,MAAOjP,IAAmB,MAMxBuK,GAAkB,SAAS2E,GAC7B,MAAqB,gBAAPA,IAAmBA,GAAM,+BAA+B5E,KAAK4E,IAMzE1C,GAAe,SAASJ,GAC1B,GAAInB,EAOJ,IANqB,gBAAVmB,IAAsBA,GAC/BnB,EAAYmB,EACZA,MAC0B,gBAAVA,IAAsBA,GAA+B,gBAAfA,GAAMX,MAAqBW,EAAMX,OACvFR,EAAYmB,EAAMX,MAEfR,EAAL,CAGAA,EAAYA,EAAUI,eACjBe,EAAMlI,SAAW,4BAA4BoG,KAAKW,IAA4B,UAAdA,GAAwC,oBAAfmB,EAAMV,QAClGU,EAAMlI,OAASjE,GAEjBwD,EAAQ2I,GACNX,KAAMR,EACN/G,OAAQkI,EAAMlI,QAAUlE,GAAmB,KAC3CmP,cAAe/C,EAAM+C,eAAiB,KACtCC,cAAe1I,GAAeA,EAAYC,QAAU,KACpD0I,UAAWjD,EAAMiD,WAAavN,KAAU,MAE1C,IAAIwN,GAAM1H,EAAewE,EAAMX,KAsC/B,OArCmB,UAAfW,EAAMX,MAAoBW,EAAMV,MAAQ4D,IAC1CA,EAAMA,EAAIlD,EAAMV,OAEd4D,IACFlD,EAAMmD,QAAUD,GAEC,UAAflD,EAAMX,MACRhI,EAAQ2I,GACNlI,OAAQ,KACR0C,QAASF,EAAYE,UAGN,UAAfwF,EAAMX,OACJ9C,EAAkC2B,KAAK8B,EAAMV,OAC/CjI,EAAQ2I,GACNlI,OAAQ,KACRsL,eAAgBlI,IAGhB2B,EAAyCqB,KAAK8B,EAAMV,OACtDjI,EAAQ2I,GACNxF,QAASF,EAAYE,WAIR,SAAfwF,EAAMX,OACRW,EAAMqD,eACJC,QAAS7E,GAAc6E,QACvBrC,UAAWxC,GAAcwC,YAGV,cAAfjB,EAAMX,OACRW,EAAQuD,GAAyBvD,EAAO3E,IAEtC2E,EAAMlI,SAAWkI,EAAM+C,gBACzB/C,EAAM+C,cAAgBS,GAAkBxD,EAAMlI,SAEzC2L,GAAczD,KAMnBwD,GAAoB,SAASE,GAC/B,GAAIC,GAAkBD,GAAYA,EAAS3B,cAAgB2B,EAAS3B,aAAa,wBACjF,OAAO4B,GAAkB5P,EAAU6P,eAAeD,GAAmB,MAMnEF,GAAgB,SAASzD,GAC3B,GAAIA,GAAS,8CAA8C9B,KAAK8B,EAAMX,MAAO,CAC3E,GAAIwE,GAAa7D,EAAMlI,OACnBgM,EAA6B,eAAf9D,EAAMX,MAAyBW,EAAM+C,cAAgB/C,EAAM+C,cAAgBrP,EACzFqQ,EAA2B,cAAf/D,EAAMX,MAAwBW,EAAM+C,cAAgB/C,EAAM+C,cAAgBrP,EACtFsQ,EAAMC,GAAoBJ,GAC1BK,EAAapQ,EAAQoQ,YAAcpQ,EAAQqQ,SAAW,EACtDC,EAAYtQ,EAAQsQ,WAAatQ,EAAQuQ,SAAW,EACpDC,EAAavQ,EAAUwQ,KAAKD,WAAavQ,EAAUyQ,gBAAgBF,WACnEG,EAAY1Q,EAAUwQ,KAAKE,UAAY1Q,EAAUyQ,gBAAgBC,UACjEC,EAAQV,EAAItB,MAAiC,gBAAlB1C,GAAM2E,QAAuB3E,EAAM2E,QAAU,GACxEC,EAAQZ,EAAI5J,KAAgC,gBAAlB4F,GAAM6E,QAAuB7E,EAAM6E,QAAU,GACvEC,EAAUJ,EAAQJ,EAClBS,EAAUH,EAAQH,EAClBN,EAAUD,EAAaY,EACvBT,EAAUD,EAAYW,EACtBC,EAAmC,gBAApBhF,GAAMiF,UAAyBjF,EAAMiF,UAAY,EAChEC,EAAmC,gBAApBlF,GAAMmF,UAAyBnF,EAAMmF,UAAY,QAC7DnF,GAAM2E,cACN3E,GAAM6E,QACbxN,EAAQ2I,GACN6D,WAAYA,EACZC,YAAaA,EACbC,UAAWA,EACXI,QAASA,EACTE,QAASA,EACTK,MAAOA,EACPE,MAAOA,EACPE,QAASA,EACTC,QAASA,EACTK,EAAGN,EACHO,EAAGN,EACHE,UAAWD,EACXG,UAAWD,EACXI,QAAS,EACTC,QAAS,EACTC,OAAQ,EACRC,OAAQ,IAGZ,MAAOzF,IAQL0F,GAAsB,SAAS1F,GACjC,GAAInB,GAAYmB,GAA+B,gBAAfA,GAAMX,MAAqBW,EAAMX,MAAQ,EACzE,QAAQ,gCAAgCnB,KAAKW,IAQ3C8G,GAAoB,SAASC,EAAMC,EAASjO,EAAMkO,GAChDA,EACF3R,EAAY,WACVyR,EAAKG,MAAMF,EAASjO,IACnB,GAEHgO,EAAKG,MAAMF,EAASjO,IASpB0I,GAAqB,SAASN,GAChC,GAAuB,gBAAVA,IAAsBA,GAASA,EAAMX,KAAlD,CAGA,GAAIyG,GAAQJ,GAAoB1F,GAC5BgG,EAAuB7K,EAAU,SACjC8K,EAAuB9K,EAAU6E,EAAMX,UACvC6G,EAAWF,EAAqBG,OAAOF,EAC3C,IAAIC,GAAYA,EAASnO,OAAQ,CAC/B,GAAIT,GAAGC,EAAKqO,EAAMC,EAAS5F,EAAWmG,EAAkB7F,IACxD,KAAKjJ,EAAI,EAAGC,EAAM2O,EAASnO,OAAYR,EAAJD,EAASA,IAC1CsO,EAAOM,EAAS5O,GAChBuO,EAAUO,EACU,gBAATR,IAA8C,kBAAlB9R,GAAQ8R,KAC7CA,EAAO9R,EAAQ8R,IAEG,gBAATA,IAAqBA,GAAoC,kBAArBA,GAAKS,cAClDR,EAAUD,EACVA,EAAOA,EAAKS,aAEM,kBAATT,KACT3F,EAAY5I,KAAY2I,GACxB2F,GAAkBC,EAAMC,GAAW5F,GAAa6F,IAItD,MAAOvF,QAOL+F,GAAkC,SAAStG,GAC7C,GAAIuG,GAAc,IAIlB,QAHIrM,KAAkB,GAAS8F,GAAwB,UAAfA,EAAMX,MAAoBW,EAAMV,MAAoE,KAA5DjD,EAAoC/D,QAAQ0H,EAAMV,SAChIiH,GAAc,GAETA,GAOLlG,GAAmB,SAASL,GAC9B,GAAI2B,GAAU3B,EAAMlI,QAAUlE,GAAmB,KAC7C4S,EAAgC,QAAlBxG,EAAMyG,OAExB,cADOzG,GAAMyG,QACLzG,EAAMX,MACb,IAAK,QACJ,GAAIkH,GAA6B,oBAAfvG,EAAMV,MAA8BgH,GAAgCtG,EAC3D,kBAAhBuG,KACTjM,EAAYM,UAAY2L,GAEwB,KAA9CjK,EAAsBhE,QAAQ0H,EAAMV,MACtCjI,EAAQiD,GACNI,SAAyB,mBAAfsF,EAAMV,KAChB3E,SAAyB,mBAAfqF,EAAMV,KAChBzE,YAA4B,sBAAfmF,EAAMV,KACnBxE,SAAyB,mBAAfkF,EAAMV,KAChBvE,YAA4B,sBAAfiF,EAAMV,KACnBtE,QAAwB,kBAAfgF,EAAMV,KACfrE,OAAO,IAEe,qBAAf+E,EAAMV,OACf3L,EAAgBqM,EAAMR,WACtBnI,EAAQiD,GACNI,UAAU,EACVC,UAAU,EACVE,aAAa,EACbC,UAAU,EACVC,aAAa,EACbC,SAAS,EACTC,OAAO,KAGXyL,IACA,MAED,KAAK,QACJ/S,EAAgBqM,EAAMR,UACtB,IAAImH,GAAiBrM,EAAYS,eAAgB,CACjD1D,GAAQiD,GACNI,UAAU,EACVC,UAAU,EACVC,WAAW,EACXC,aAAa,EACbC,UAAU,EACVC,aAAa,EACbC,QAAS2L,EACT1L,OAAQ0L,IAEVD,IACA,MAED,KAAK,aACJ7S,EAAc8N,CACd,MAED,KAAK,OACJ,GAAIiF,GAAaC,EAAanD,EAAW1D,EAAM+C,eACzC3H,EAAU,eAAgBA,EAAU,eAAkBsI,IAAamD,EAAcnD,EAASoD,OAASpD,EAASqD,WAAarD,EAASsD,aAAeJ,EAAclD,EAASoD,OAASpD,EAASkD,aAAelD,EAASuD,YACtNjH,EAAMqD,cAAcpC,YACpBjB,EAAMqD,cAAcC,QAAQ,aAAcsD,GACtCC,IAAgBD,GAClB5G,EAAMqD,cAAcC,QAAQ,YAAauD,KAEjCzL,EAAU,eAAiB4E,EAAMlI,SAAW8O,EAAc5G,EAAMlI,OAAOiK,aAAa,0BAC9F/B,EAAMqD,cAAcpC,YACpBjB,EAAMqD,cAAcC,QAAQ,aAAcsD,GAE5C,MAED,KAAK,YACJM,GAA0BlH,GAC1BvB,GAAcwC,YACVU,GAAWA,IAAYwF,MAAwBxF,EAAQyF,OACzDzF,EAAQyF,OAEV,MAED,KAAK,aACJ3I,GAAc2I,MAAMzF,GAChB7E,EAAcS,gBAAiB,GAAQiJ,IACrC7E,GAAWA,IAAY3B,EAAM+C,gBAAkBvK,EAAawH,EAAM+C,cAAepB,IACnF0F,GAAgBhQ,KAAY2I,GAC1BX,KAAM,aACNiI,SAAS,EACTC,YAAY,KAGhBF,GAAgBhQ,KAAY2I,GAC1BX,KAAM,eAGV,MAED,KAAK,YACJZ,GAAcyC,OACVpE,EAAcS,gBAAiB,GAAQiJ,IACrC7E,GAAWA,IAAY3B,EAAM+C,gBAAkBvK,EAAawH,EAAM+C,cAAepB,IACnF0F,GAAgBhQ,KAAY2I,GAC1BX,KAAM,aACNiI,SAAS,EACTC,YAAY,KAGhBF,GAAgBhQ,KAAY2I,GAC1BX,KAAM,cAGV,MAED,KAAK,aACJwC,GAAUF,EAAS7E,EAAcc,aAC7Bd,EAAcS,gBAAiB,GAAQiJ,GACzCa,GAAgBhQ,KAAY2I,GAC1BX,KAAMW,EAAMX,KAAK9I,MAAM,KAG3B,MAED,KAAK,WACJqL,GAAaD,EAAS7E,EAAcc,aAChCd,EAAcS,gBAAiB,GAAQiJ,GACzCa,GAAgBhQ,KAAY2I,GAC1BX,KAAMW,EAAMX,KAAK9I,MAAM,KAG3B,MAED,KAAK,SACJ1C,EAAc,KACViJ,EAAcS,gBAAiB,GAAQiJ,GACzCa,GAAgBhQ,KAAY2I,GAC1BX,KAAMW,EAAMX,KAAK9I,MAAM,KAG3B,MAED,KAAK,aACAuG,EAAcS,gBAAiB,GAAQiJ,GACzCa,GAAgBhQ,KAAY2I,GAC1BX,KAAMW,EAAMX,KAAK9I,MAAM,MAK7B,MAAI,8CAA8C2H,KAAK8B,EAAMX,OACpD,EADT,QAQE6H,GAA4B,SAASM,GACvC,GAAIA,EAAeC,QAAUD,EAAeC,OAAO1P,OAAS,EAAG,CAC7D,GAAI2P,GAAa1P,EAAUwP,EAC3BnQ,GAAQqQ,GACNrI,KAAM,QACNC,KAAM,0BAEDoI,GAAWC,QAClBxT,EAAY,WACVsK,GAAcW,KAAKsI,IAClB,KASHL,GAAkB,SAASrH,GAC7B,GAAMA,GAA+B,gBAAfA,GAAMX,MAAqBW,EAAjD,CAGA,GAAI/I,GAAGa,EAASkI,EAAMlI,QAAU,KAAM8P,EAAM9P,GAAUA,EAAOY,eAAiB3E,EAAW8T,GACvFC,KAAMF,EAAIG,aAAejU,EACzBkU,WAAW,EACXT,YAAY,EACZU,OAAuB,UAAfjI,EAAMX,KAAmB,EAAI,EACrC6I,OAA+B,gBAAhBlI,GAAMmI,MAAqBnI,EAAMmI,MAAQ,EAA4B,gBAAjBnI,GAAMkI,OAAsBlI,EAAMkI,OAASN,EAAIQ,YAAc,EAAI,GACnIxQ,EAAOP,EAAQwQ,EAAU7H,EACvBlI,IAGD8P,EAAIQ,aAAetQ,EAAOuQ,gBAC5BzQ,GAASA,EAAKyH,KAAMzH,EAAKoQ,UAAWpQ,EAAK2P,WAAY3P,EAAKkQ,KAAMlQ,EAAKqQ,OAAQrQ,EAAKuM,QAASvM,EAAKyM,QAASzM,EAAKkN,QAASlN,EAAKmN,QAASnN,EAAK0Q,QAAS1Q,EAAK2Q,OAAQ3Q,EAAK4Q,SAAU5Q,EAAK6Q,QAAS7Q,EAAKsQ,OAAQtQ,EAAKmL,eAC/M9L,EAAI2Q,EAAIQ,YAAY,eAChBnR,EAAEyR,iBACJzR,EAAEyR,eAAe3C,MAAM9O,EAAGW,GAC1BX,EAAEwP,QAAU,KACZ3O,EAAOuQ,cAAcpR,OAoBvB0R,GAA8B,WAChC,GAAI7H,GAAUhE,EAAcO,gBAC5B,IAAuB,gBAAZyD,IAAwBA,GAAW,EAAG,CAC/C,GAAI8H,GAAWC,KAAKC,IAAI,IAAKhI,EAAU,IACnCiI,EAAoBjM,EAAcY,YAAc,kBACpDnC,GAA4BhH,EAAa,WACvC,GAAImC,GAAK3C,EAAU6P,eAAemF,EAC9BC,IAAkBtS,KACpBgQ,KACApM,EAAYS,YAAc,KAC1B0D,GAAcW,MACZC,KAAM,QACNC,KAAM,oBAGTsJ,KAOHK,GAAoB,WACtB,GAAIC,GAAYnV,EAAU+C,cAAc,MASxC,OARAoS,GAAUpG,GAAKhG,EAAcU,YAC7B0L,EAAUC,UAAYrM,EAAcW,eACpCyL,EAAUzG,MAAM2G,SAAW,WAC3BF,EAAUzG,MAAMC,KAAO,MACvBwG,EAAUzG,MAAMrI,IAAM,UACtB8O,EAAUzG,MAAME,MAAQ,MACxBuG,EAAUzG,MAAMG,OAAS,MACzBsG,EAAUzG,MAAM1E,OAAS,GAAKsL,GAAevM,EAAciB,QACpDmL,GAMLjH,GAAiB,SAASqH,GAE5B,IADA,GAAItH,GAAasH,GAAeA,EAAY3Q,WACrCqJ,GAAsC,WAAxBA,EAAWuH,UAAyBvH,EAAWrJ,YAClEqJ,EAAaA,EAAWrJ,UAE1B,OAAOqJ,IAAc,MAQnBjB,GAAY,WACd,GAAIxJ,GAAK+R,EAAchP,EAAYC,OAAQ2O,EAAYjH,GAAeqH,EACtE,KAAKA,EAAa,CAChB,GAAIE,GAAoBC,GAAuB3V,EAAQmJ,SAASC,KAAMJ,GAClE4M,EAAwC,UAAtBF,EAAgC,OAAS,MAC3DG,EAAYC,GAAMvS,GACpBkI,UAAWd,GAAcjE,SACxBsC,IACC+M,EAAS/M,EAAcC,QAAU+M,GAAWhN,EAAcC,QAASD,EACvEoM,GAAYD,IACZ,IAAIc,GAAkBhW,EAAU+C,cAAc,MAC9CoS,GAAUc,YAAYD,GACtBhW,EAAUwQ,KAAKyF,YAAYd,EAC3B,IAAIe,GAASlW,EAAU+C,cAAc,OACjCoT,EAA0C,YAA3B5P,EAAYG,UAC/BwP,GAAOjD,UAAY,eAAiBlK,EAAcY,YAAc,WAAaZ,EAAcY,YAAc,iCAAwCwM,EAAe,uDAAyD,8CAAgDL,EAAS,KAAO,KAAOK,EAAe,8BAAgCL,EAAS,MAAQ,IAAM,0CAA4CL,EAAoB,2CAAkDE,EAAkB,gHAAiIC,EAAY,eAAsB7M,EAAcY,YAAc,0CACzqB4L,EAAcW,EAAOE,WACrBF,EAAS,KACTzT,EAAQ8S,GAAa7K,cAAgBA,GACrCyK,EAAUkB,aAAad,EAAaS,GACpCpB,KAYF,MAVKW,KACHA,EAAcvV,EAAU+I,EAAcY,aAClC4L,IAAgB/R,EAAM+R,EAAYvR,UACpCuR,EAAcA,EAAY/R,EAAM,KAE7B+R,GAAeJ,IAClBI,EAAcJ,EAAUiB,aAG5B7P,EAAYC,OAAS+O,GAAe,KAC7BA,GAMLnI,GAAc,WAChB,GAAImI,GAAchP,EAAYC,MAC9B,IAAI+O,EAAa,CACf,GAAItH,GAAaC,GAAeqH,EAC5BtH,KAC6B,YAA3B1H,EAAYG,YAA4B,cAAgB6O,IAC1DA,EAAY7G,MAAM4H,QAAU,OAC5B,QAAUC,KACR,GAA+B,IAA3BhB,EAAYxP,WAAkB,CAChC,IAAK,GAAIrC,KAAQ6R,GACkB,kBAAtBA,GAAY7R,KACrB6R,EAAY7R,GAAQ,KAGpB6R,GAAY3Q,YACd2Q,EAAY3Q,WAAW4R,YAAYjB,GAEjCtH,EAAWrJ,YACbqJ,EAAWrJ,WAAW4R,YAAYvI,OAGpC7N,GAAYmW,EAAiB,SAI7BhB,EAAY3Q,YACd2Q,EAAY3Q,WAAW4R,YAAYjB,GAEjCtH,EAAWrJ,YACbqJ,EAAWrJ,WAAW4R,YAAYvI,KAIxC0E,KACApM,EAAYW,MAAQ,KACpBX,EAAYC,OAAS,KACrBD,EAAYS,YAAc,KAC1BpH,EAAgBD,IAShB8M,GAAsB,SAASgK,GACjC,GAAIC,MAAkB/J,IACtB,IAA0B,gBAAb8J,IAAyBA,EAAtC,CAGA,IAAK,GAAIjJ,KAAciJ,GACrB,GAAIjJ,GAAcrL,EAAQkB,KAAKoT,EAAUjJ,IAA+C,gBAAzBiJ,GAASjJ,IAA4BiJ,EAASjJ,GAC3G,OAAQA,EAAWtC,eAClB,IAAK,aACL,IAAK,OACL,IAAK,WACL,IAAK,aACJwL,EAAYC,KAAOF,EAASjJ,GAC5Bb,EAAUgK,KAAOnJ,CACjB,MAED,KAAK,YACL,IAAK,OACL,IAAK,WACL,IAAK,aACJkJ,EAAYE,KAAOH,EAASjJ,GAC5Bb,EAAUiK,KAAOpJ,CACjB,MAED,KAAK,kBACL,IAAK,WACL,IAAK,MACL,IAAK,WACL,IAAK,UACL,IAAK,YACJkJ,EAAYG,IAAMJ,EAASjJ,GAC3Bb,EAAUkK,IAAMrJ,EAQtB,OACEd,KAAMgK,EACN/J,UAAWA,KASX6C,GAA2B,SAASsH,EAAanK,GACnD,GAA6B,gBAAhBmK,KAA4BA,GAAoC,gBAAdnK,KAA0BA,EACvF,MAAOmK,EAET,IAAIC,KACJ,KAAK,GAAIrT,KAAQoT,GACf,GAAI3U,EAAQkB,KAAKyT,EAAapT,GAC5B,GAAa,WAATA,EAAmB,CACrBqT,EAAWrT,GAAQoT,EAAYpT,GAAQoT,EAAYpT,GAAMlB,UACzD,KAAK,GAAIe,GAAI,EAAGC,EAAMuT,EAAWrT,GAAMM,OAAYR,EAAJD,EAASA,IACtDwT,EAAWrT,GAAMH,GAAG+J,OAASX,EAAUoK,EAAWrT,GAAMH,GAAG+J,YAExD,IAAa,YAAT5J,GAA+B,SAATA,EAC/BqT,EAAWrT,GAAQoT,EAAYpT,OAC1B,CACLqT,EAAWrT,KACX,IAAIsT,GAAUF,EAAYpT,EAC1B,KAAK,GAAI8J,KAAcwJ,GACjBxJ,GAAcrL,EAAQkB,KAAK2T,EAASxJ,IAAerL,EAAQkB,KAAKsJ,EAAWa,KAC7EuJ,EAAWrT,GAAMiJ,EAAUa,IAAewJ,EAAQxJ,IAM5D,MAAOuJ,IAULhB,GAAa,SAASkB,EAAM/M,GAC9B,GAAId,GAAuB,MAAXc,GAAmBA,GAAWA,EAAQd,aAAc,CACpE,OAAIA,IAC4B,KAAtB6N,EAAK1S,QAAQ,KAAc,IAAM,KAAO,WAAa5C,IAEtD,IAUPkU,GAAQ,SAAS3L,GACnB,GAAI3G,GAAGC,EAAK0T,EAAQC,EAASC,EAAM,GAAIC,IAQvC,IAPInN,EAAQjB,iBAC4B,gBAA3BiB,GAAQjB,eACjBkO,GAAYjN,EAAQjB,gBACuB,gBAA3BiB,GAAQjB,gBAA+B,UAAYiB,GAAQjB,iBAC3EkO,EAAUjN,EAAQjB,iBAGlBkO,GAAWA,EAAQnT,OACrB,IAAKT,EAAI,EAAGC,EAAM2T,EAAQnT,OAAYR,EAAJD,EAASA,IACzC,GAAIpB,EAAQkB,KAAK8T,EAAS5T,IAAM4T,EAAQ5T,IAA4B,gBAAf4T,GAAQ5T,GAAiB,CAE5E,GADA2T,EAASI,GAAeH,EAAQ5T,KAC3B2T,EACH,QAEF,IAAe,MAAXA,EAAgB,CAClBG,EAAuBrT,OAAS,EAChCqT,EAAuBjM,KAAK8L,EAC5B,OAEFG,EAAuBjM,KAAK4G,MAAMqF,GAA0BH,EAAQ,KAAOA,EAAQnX,EAAQmJ,SAASqO,SAAW,KAAOL,IAgB5H,MAZIG,GAAuBrT,SACzBoT,GAAO,kBAAoBtW,EAAoBuW,EAAuBxO,KAAK,OAEzEqB,EAAQb,0BAA2B,IACrC+N,IAAQA,EAAM,IAAM,IAAM,+BAEO,gBAAxBlN,GAAQP,aAA4BO,EAAQP,cACrDyN,IAAQA,EAAM,IAAM,IAAM,eAAiBtW,EAAoBoJ,EAAQP,cAExC,gBAAtBO,GAAQsB,WAA0BtB,EAAQsB,YACnD4L,IAAQA,EAAM,IAAM,IAAM,aAAetW,EAAoBoJ,EAAQsB,YAEhE4L,GASLE,GAAiB,SAASE,GAC5B,GAAmB,MAAfA,GAAuC,KAAhBA,EACzB,MAAO,KAGT,IADAA,EAAcA,EAAY5O,QAAQ,aAAc,IAC5B,KAAhB4O,EACF,MAAO,KAET,IAAIC,GAAgBD,EAAYjT,QAAQ,KACxCiT,GAAgC,KAAlBC,EAAuBD,EAAcA,EAAYhV,MAAMiV,EAAgB,EACrF,IAAIC,GAAYF,EAAYjT,QAAQ,IAEpC,OADAiT,GAA4B,KAAdE,EAAmBF,EAAgC,KAAlBC,GAAsC,IAAdC,EAAkB,KAAOF,EAAYhV,MAAM,EAAGkV,GACjHF,GAAuD,SAAxCA,EAAYhV,MAAM,IAAI0I,cAChC,KAEFsM,GAAe,MAQpB9B,GAAyB,WAC3B,GAAIiC,GAAqB,SAASC,GAChC,GAAIrU,GAAGC,EAAK4I,EAAKyL,IAIjB,IAHuB,gBAAZD,KACTA,GAAYA,IAEW,gBAAZA,KAAwBA,GAAqC,gBAAnBA,GAAQ5T,OAC7D,MAAO6T,EAET,KAAKtU,EAAI,EAAGC,EAAMoU,EAAQ5T,OAAYR,EAAJD,EAASA,IACzC,GAAIpB,EAAQkB,KAAKuU,EAASrU,KAAO6I,EAAMkL,GAAeM,EAAQrU,KAAM,CAClE,GAAY,MAAR6I,EAAa,CACfyL,EAAa7T,OAAS,EACtB6T,EAAazM,KAAK,IAClB,OAEgC,KAA9ByM,EAAatT,QAAQ6H,IACvByL,EAAazM,KAAKgB,GAIxB,MAAOyL,GAET,OAAO,UAASC,EAAeC,GAC7B,GAAIC,GAAYV,GAAeS,EAAc/O,QAC3B,QAAdgP,IACFA,EAAYF,EAEd,IAAI7O,GAAiB0O,EAAmBI,EAAc9O,gBAClDzF,EAAMyF,EAAejF,MACzB,IAAIR,EAAM,EAAG,CACX,GAAY,IAARA,GAAmC,MAAtByF,EAAe,GAC9B,MAAO,QAET,IAA8C,KAA1CA,EAAe1E,QAAQuT,GACzB,MAAY,KAARtU,GAAasU,IAAkBE,EAC1B,aAEF,SAGX,MAAO,YASP5E,GAAqB,WACvB,IACE,MAAOpT,GAAUiY,cACjB,MAAO1S,GACP,MAAO,QASPuI,GAAY,SAASF,EAASmF,GAChC,GAAImF,GAAGC,EAAI/C,EAAWgD,IAItB,IAHqB,gBAAVrF,IAAsBA,IAC/BqF,EAAarF,EAAM/N,MAAM,QAEvB4I,GAAgC,IAArBA,EAAQ3K,UAAkBmV,EAAWpU,OAAS,EAC3D,GAAI4J,EAAQyK,UACV,IAAKH,EAAI,EAAGC,EAAKC,EAAWpU,OAAYmU,EAAJD,EAAQA,IAC1CtK,EAAQyK,UAAUC,IAAIF,EAAWF,QAE9B,IAAItK,EAAQvL,eAAe,aAAc,CAE9C,IADA+S,EAAY,IAAMxH,EAAQwH,UAAY,IACjC8C,EAAI,EAAGC,EAAKC,EAAWpU,OAAYmU,EAAJD,EAAQA,IACW,KAAjD9C,EAAU7Q,QAAQ,IAAM6T,EAAWF,GAAK,OAC1C9C,GAAagD,EAAWF,GAAK,IAGjCtK,GAAQwH,UAAYA,EAAUxM,QAAQ,aAAc,IAGxD,MAAOgF,IAQLC,GAAe,SAASD,EAASmF,GACnC,GAAImF,GAAGC,EAAI/C,EAAWgD,IAItB,IAHqB,gBAAVrF,IAAsBA,IAC/BqF,EAAarF,EAAM/N,MAAM,QAEvB4I,GAAgC,IAArBA,EAAQ3K,UAAkBmV,EAAWpU,OAAS,EAC3D,GAAI4J,EAAQyK,WAAazK,EAAQyK,UAAUrU,OAAS,EAClD,IAAKkU,EAAI,EAAGC,EAAKC,EAAWpU,OAAYmU,EAAJD,EAAQA,IAC1CtK,EAAQyK,UAAUE,OAAOH,EAAWF,QAEjC,IAAItK,EAAQwH,UAAW,CAE5B,IADAA,GAAa,IAAMxH,EAAQwH,UAAY,KAAKxM,QAAQ,YAAa,KAC5DsP,EAAI,EAAGC,EAAKC,EAAWpU,OAAYmU,EAAJD,EAAQA,IAC1C9C,EAAYA,EAAUxM,QAAQ,IAAMwP,EAAWF,GAAK,IAAK,IAE3DtK,GAAQwH,UAAYA,EAAUxM,QAAQ,aAAc,IAGxD,MAAOgF,IAULS,GAAY,SAAS1L,EAAIe,GAC3B,GAAIqP,GAAQnS,EAAkB+B,EAAI,MAAM6V,iBAAiB9U,EACzD,OAAa,WAATA,GACGqP,GAAmB,SAAVA,GACQ,MAAhBpQ,EAAG6S,SAKJzC,EAJM,WAYX7C,GAAsB,SAASvN,GACjC,GAAIsN,IACFtB,KAAM,EACNtI,IAAK,EACLuI,MAAO,EACPC,OAAQ,EAEV,IAAIlM,EAAG8V,sBAAuB,CAC5B,GAAIC,GAAS/V,EAAG8V,wBACZE,EAAc5Y,EAAQ4Y,YACtBC,EAAc7Y,EAAQ6Y,YACtBC,EAAkB7Y,EAAUyQ,gBAAgBqI,YAAc,EAC1DC,EAAiB/Y,EAAUyQ,gBAAgBuI,WAAa,EACxDC,EAAiB,EACjBC,EAAgB,CACpB,IAA8C,aAA1C7K,GAAUrO,EAAUwQ,KAAM,YAA4B,CACxD,GAAI2I,GAAWnZ,EAAUwQ,KAAKiI,wBAC1BW,EAAWpZ,EAAUyQ,gBAAgBgI,uBACzCQ,GAAiBE,EAASxK,KAAOyK,EAASzK,MAAQ,EAClDuK,EAAgBC,EAAS9S,IAAM+S,EAAS/S,KAAO,EAEjD4J,EAAItB,KAAO+J,EAAO/J,KAAOgK,EAAcE,EAAkBI,EACzDhJ,EAAI5J,IAAMqS,EAAOrS,IAAMuS,EAAcG,EAAiBG,EACtDjJ,EAAIrB,MAAQ,SAAW8J,GAASA,EAAO9J,MAAQ8J,EAAOW,MAAQX,EAAO/J,KACrEsB,EAAIpB,OAAS,UAAY6J,GAASA,EAAO7J,OAAS6J,EAAOY,OAASZ,EAAOrS,IAE3E,MAAO4J,IAQLgF,GAAoB,SAAStS,GAC/B,IAAKA,EACH,OAAO,CAET,IAAI4W,GAAS3Y,EAAkB+B,EAAI,MAC/B6W,EAAejY,EAAYgY,EAAO1K,QAAU,EAC5C4K,EAAclY,EAAYgY,EAAO3K,OAAS,EAC1C8K,EAAYnY,EAAYgY,EAAOlT,MAAQ,EACvCsT,EAAapY,EAAYgY,EAAO5K,OAAS,EACzCiL,EAAWJ,GAAgBC,GAAeC,GAAaC,EACvDE,EAAOD,EAAW,KAAO1J,GAAoBvN,GAC7CmX,EAA+B,SAAnBP,EAAOjD,SAA4C,aAAtBiD,EAAOQ,aAA8BH,KAAcC,IAASL,GAAgBK,EAAKhL,OAAS,KAAO4K,GAAeI,EAAKjL,MAAQ,KAAO8K,GAAaG,EAAKxT,KAAO,KAAOsT,GAAcE,EAAKlL,MAAQ,GAC5O,OAAOmL,IAQLnH,GAA2B,WAC7BrS,EAAciH,GACdA,EAAqB,EACrB7G,EAAe8G,GACfA,EAA4B,GAQ1B+G,GAAc,WAChB,GAAIN,EACJ,IAAIpO,IAAoBoO,EAAaC,GAAe3H,EAAYC,SAAU,CACxE,GAAIyJ,GAAMC,GAAoBrQ,EAC9ByD,GAAQ2K,EAAWS,OACjBE,MAAOqB,EAAIrB,MAAQ,KACnBC,OAAQoB,EAAIpB,OAAS,KACrBxI,IAAK4J,EAAI5J,IAAM,KACfsI,KAAMsB,EAAItB,KAAO,KACjB3E,OAAQ,GAAKsL,GAAevM,EAAciB,YAU5CsE,GAAiB,SAAS0L,GACxBzT,EAAYW,SAAU,IACpBX,EAAYC,QAAsD,kBAArCD,GAAYC,OAAOyT,cAClD1T,EAAYC,OAAOyT,cAAcD,GAEjCzT,EAAYW,OAAQ,IAUtBoO,GAAiB,SAAS4E,GAC5B,GAAI,qBAAqB/P,KAAK+P,GAC5B,MAAOA,EAET,IAAIlQ,EAMJ,OALmB,gBAARkQ,IAAqBzY,EAAOyY,GAEb,gBAARA,KAChBlQ,EAASsL,GAAelU,EAAU8Y,EAAK,MAFvClQ,EAASkQ,EAIc,gBAAXlQ,GAAsBA,EAAS,QAa3CM,GAAiB,SAAS6P,GAC5B,GAAIC,GAAuBC,EAAOC,EAAYzN,EAAgBtG,EAAYM,UAAW2L,EAAc,IAEnG,IADA2H,EAA4BA,KAA8B,EACtDhU,KAAkB,EACpBqM,GAAc,MACT,CACL,IACE6H,EAAQ3a,EAAO6a,cAAgB,KAC/B,MAAOrX,GACPoX,GACE/O,KAAMrI,EAAEqI,KACR6D,QAASlM,EAAEkM,SAGf,GAAIiL,GAA4B,IAAnBA,EAAMpX,UAAqC,WAAnBoX,EAAM7E,SACzC,IACEhD,EAAc6H,EAAMG,aAAa,WACjC,MAAOtX,GACPsP,EAAc,SAEX,CACL,IACE4H,EAAwBna,SAASiX,QAAU,KAC3C,MAAOhU,GACPkX,EAAwB,MAEI,OAA1BA,GAAkCE,GAAkC,kBAApBA,EAAW/O,MAA4B,kDAAkDpB,KAAKmQ,EAAWlL,QAAQlE,kBACnKsH,GAAc,IAQpB,MAJAjM,GAAYM,UAAY2L,EACpB3F,IAAkB2F,GAAgB2H,GACpCM,GAAoBzZ,GAEfwR,GAWLiI,GAAsB,SAASxZ,GAQjC,QAASyZ,GAAkBC,GACzB,GAAIvV,GAAUuV,EAAKtV,MAAM,SAEzB,OADAD,GAAQpB,OAAS,EACVoB,EAAQyD,KAAK,KAEtB,QAAS+R,GAAcC,GACrB,QAASA,IAAwBA,EAAsBA,EAAoB3P,iBAAmB,0EAA0Ef,KAAK0Q,IAA2D,kBAAnCA,EAAoBrY,MAAM,MAEjO,QAASsY,GAAcC,GACjBA,IACFC,GAAW,EACPD,EAAOtU,UACTwU,EAAeP,EAAkBK,EAAOtU,WAErCwU,GAAgBF,EAAOG,cAC1BD,EAAeP,EAAkBK,EAAOG,cAEtCH,EAAOI,WACTC,EAAUR,EAAcG,EAAOI,YAzBrC,GAAIJ,GAAQM,EAAIC,EAAUN,GAAW,EAAOO,GAAY,EAAOH,GAAU,EAAOH,EAAe,EA6B/F,IAAI/a,EAAWsb,SAAWtb,EAAWsb,QAAQxX,OAC3C+W,EAAS7a,EAAWsb,QAAQ,mBAC5BV,EAAcC,GACV7a,EAAWsb,QAAQ,yBACrBR,GAAW,EACXC,EAAe,gBAEZ,IAAI/a,EAAWub,WAAavb,EAAWub,UAAUzX,OACtDsX,EAAWpb,EAAWub,UAAU,iCAChCV,EAASO,GAAYA,EAASI,cAC9BZ,EAAcC,OACT,IAA6B,mBAAlB9Z,GAA+B,CAC/Csa,GAAY,CACZ,KACEF,EAAK,GAAIpa,GAAc,mCACvB+Z,GAAW,EACXC,EAAeP,EAAkBW,EAAGM,YAAY,aAChD,MAAOC,GACP,IACEP,EAAK,GAAIpa,GAAc,mCACvB+Z,GAAW,EACXC,EAAe,SACf,MAAOY,GACP,IACER,EAAK,GAAIpa,GAAc,iCACvB+Z,GAAW,EACXC,EAAeP,EAAkBW,EAAGM,YAAY,aAChD,MAAOG,GACPP,GAAY,KAKpBhV,EAAYI,SAAWqU,KAAa,EACpCzU,EAAYK,SAAWqU,GAAgB1Z,EAAY0Z,GAAgB1Z,EAAY4F,GAC/EZ,EAAYE,QAAUwU,GAAgB,QACtC1U,EAAYG,WAAa0U,EAAU,SAAWG,EAAY,UAAYP,EAAW,WAAa,UAKhGP,IAAoBzZ,GAIpBsJ,IAAe,EAMf,IAAII,IAAgB,WAClB,MAAM8B,gBAAgB9B,SAGqB,kBAAhCA,IAAcqR,eACvBrR,GAAcqR,cAAc/J,MAAMxF,KAAMrJ,EAAMW,aAHvC,GAAI4G,IAafzI,GAAgByI,GAAe,WAC7BqI,MAAO,QACPiJ,UAAU,EACVC,cAAc,EACdC,YAAY,IASdxR,GAAcC,OAAS,WACrB,MAAOV,GAAQ+H,MAAMxF,KAAMrJ,EAAMW,aAQnC4G,GAAcyR,MAAQ,WACpB,MAAO9R,GAAO2H,MAAMxF,KAAMrJ,EAAMW,aAQlC4G,GAAcoC,gBAAkB,WAC9B,MAAOlC,IAAiBoH,MAAMxF,KAAMrJ,EAAMW,aAQ5C4G,GAAcS,GAAK,WACjB,MAAON,IAAImH,MAAMxF,KAAMrJ,EAAMW,aAU/B4G,GAAcmB,IAAM,WAClB,MAAOH,IAAKsG,MAAMxF,KAAMrJ,EAAMW,aAQhC4G,GAAcyH,SAAW,WACvB,MAAOpG,IAAWiG,MAAMxF,KAAMrJ,EAAMW,aAQtC4G,GAAcW,KAAO,WACnB,MAAOW,IAAMgG,MAAMxF,KAAMrJ,EAAMW,aAQjC4G,GAAc0R,OAAS,WACrB,MAAOxP,IAAQoF,MAAMxF,KAAMrJ,EAAMW,aAQnC4G,GAAc2R,QAAU,WACtB,MAAOpP,IAAS+E,MAAMxF,KAAMrJ,EAAMW,aAQpC4G,GAAc6E,QAAU,WACtB,MAAOlC,IAAS2E,MAAMxF,KAAMrJ,EAAMW,aASpC4G,GAAcwC,UAAY,WACxB,MAAOO,IAAWuE,MAAMxF,KAAMrJ,EAAMW,aAStC4G,GAAc4R,QAAU,WACtB,MAAO5O,IAASsE,MAAMxF,KAAMrJ,EAAMW,aAWpC4G,GAAc2I,MAAQ3I,GAAc6R,SAAW,WAC7C,MAAO5O,IAAOqE,MAAMxF,KAAMrJ,EAAMW,aAUlC4G,GAAcyC,KAAOzC,GAAc8R,WAAa,WAC9C,MAAOhO,IAAMwD,MAAMxF,KAAMrJ,EAAMW,aAQjC4G,GAAcuN,cAAgB,WAC5B,MAAOnJ,IAAekD,MAAMxF,KAAMrJ,EAAMW,YAK1C,IAAI2Y,IAAmB,EAWnBC,MAIAC,GAAoB,EAOpBC,MAaAC,KAIJvZ,GAAQyF,GACNQ,cAAc,GAMhB,IAAIuT,IAAqB,SAASC,GAChC,GAAIC,GAASxQ,IACbwQ,GAAOjO,GAAK,GAAK0N,KACjBC,GAAYM,EAAOjO,KACjBkO,SAAUD,EACVD,YACA5K,aAEE4K,GACFC,EAAOE,KAAKH,GAEdrS,GAAcS,GAAG,IAAK,SAASc,GAC7B,MAAO+Q,GAAO3R,KAAKY,KAErBvB,GAAcS,GAAG,UAAW,WAC1B6R,EAAOX,YAET3R,GAAc0R,UAMZe,GAAY,SAASrS,EAAWC,GAClC,GAAIxH,GAAGC,EAAKwH,EAAQC,KAAYmS,EAAOV,GAAYlQ,KAAKuC,IAAKoD,EAAWiL,GAAQA,EAAKjL,QACrF,KAAKiL,EACH,KAAM,IAAIjc,OAAM,gFAElB,IAAyB,gBAAd2J,IAA0BA,EACnCE,EAASF,EAAUI,cAAclG,MAAM,WAClC,IAAyB,gBAAd8F,IAA0BA,GAAiC,mBAAbC,GAC9D,IAAKxH,IAAKuH,GACJ3I,EAAQkB,KAAKyH,EAAWvH,IAAmB,gBAANA,IAAkBA,GAA6B,kBAAjBuH,GAAUvH,IAC/EiJ,KAAKrB,GAAG5H,EAAGuH,EAAUvH,GAI3B,IAAIyH,GAAUA,EAAOhH,OAAQ,CAC3B,IAAKT,EAAI,EAAGC,EAAMwH,EAAOhH,OAAYR,EAAJD,EAASA,IACxCuH,EAAYE,EAAOzH,GAAGqF,QAAQ,MAAO,IACrCqC,EAAMH,IAAa,EACdqH,EAASrH,KACZqH,EAASrH,OAEXqH,EAASrH,GAAWM,KAAKL,EAQ3B,IANIE,EAAM/D,OAASX,EAAYW,OAC7BsF,KAAKnB,MACHC,KAAM,QACN0R,OAAQxQ,OAGRvB,EAAMvD,MAAO,CACf,IAAKnE,EAAI,EAAGC,EAAM+E,EAAsBvE,OAAYR,EAAJD,EAASA,IACvD,GAAIgD,EAAYgC,EAAsBhF,GAAGqF,QAAQ,UAAW,KAAM,CAChE4D,KAAKnB,MACHC,KAAM,QACNC,KAAMhD,EAAsBhF,GAC5ByZ,OAAQxQ,MAEV,OAGA5M,IAAkBD,GAAa+K,GAAcjE,UAAY7G,GAC3D4M,KAAKnB,MACHC,KAAM,QACNC,KAAM,mBACNC,UAAWd,GAAcjE,QACzBgF,WAAY7L,KAKpB,MAAO4M,OAML6Q,GAAa,SAASvS,EAAWC,GACnC,GAAIxH,GAAGC,EAAKmI,EAAYX,EAAQY,EAAkBwR,EAAOV,GAAYlQ,KAAKuC,IAAKoD,EAAWiL,GAAQA,EAAKjL,QACvG,KAAKA,EACH,MAAO3F,KAET,IAAyB,IAArB1I,UAAUE,OACZgH,EAASlJ,EAAMqQ,OACV,IAAyB,gBAAdrH,IAA0BA,EAC1CE,EAASF,EAAU9F,MAAM,WACpB,IAAyB,gBAAd8F,IAA0BA,GAAiC,mBAAbC,GAC9D,IAAKxH,IAAKuH,GACJ3I,EAAQkB,KAAKyH,EAAWvH,IAAmB,gBAANA,IAAkBA,GAA6B,kBAAjBuH,GAAUvH,IAC/EiJ,KAAKX,IAAItI,EAAGuH,EAAUvH,GAI5B,IAAIyH,GAAUA,EAAOhH,OACnB,IAAKT,EAAI,EAAGC,EAAMwH,EAAOhH,OAAYR,EAAJD,EAASA,IAGxC,GAFAuH,EAAYE,EAAOzH,GAAG2H,cAActC,QAAQ,MAAO,IACnDgD,EAAmBuG,EAASrH,GACxBc,GAAoBA,EAAiB5H,OACvC,GAAI+G,EAEF,IADAY,EAAaC,EAAiBrH,QAAQwG,GAChB,KAAfY,GACLC,EAAiBE,OAAOH,EAAY,GACpCA,EAAaC,EAAiBrH,QAAQwG,EAAUY,OAGlDC,GAAiB5H,OAAS,CAKlC,OAAOwI,OAML8Q,GAAmB,SAASxS,GAC9B,GAAIlH,GAAO,KAAMuO,EAAWuK,GAAYlQ,KAAKuC,KAAO2N,GAAYlQ,KAAKuC,IAAIoD,QAQzE,OAPIA,KAEAvO,EADuB,gBAAdkH,IAA0BA,EAC5BqH,EAASrH,GAAaqH,EAASrH,GAAWtI,MAAM,MAEhDyB,EAAUkO,IAGdvO,GAML2Z,GAAc,SAAStR,GACzB,GAAIuR,GAAkBna,KAAKmJ,KAAMP,GAAQ,CAClB,gBAAVA,IAAsBA,GAA+B,gBAAfA,GAAMX,MAAqBW,EAAMX,OAChFW,EAAQ3I,KAAY2I,GAEtB,IAAIC,GAAY5I,KAAY+I,GAAaJ,IACvC+Q,OAAQxQ,MAEViR,IAAyBpa,KAAKmJ,KAAMN,GAEtC,MAAOM,OAMLkR,GAAc,SAASX,GACzB,IAAKL,GAAYlQ,KAAKuC,IACpB,KAAM,IAAI5N,OAAM,4EAElB4b,GAAWY,GAAUZ,EACrB,KAAK,GAAIxZ,GAAI,EAAGA,EAAIwZ,EAAS/Y,OAAQT,IACnC,GAAIpB,EAAQkB,KAAK0Z,EAAUxZ,IAAMwZ,EAASxZ,IAA+B,IAAzBwZ,EAASxZ,GAAGN,SAAgB,CACrE8Z,EAASxZ,GAAGqa,aAMsD,KAA5DhB,GAAaG,EAASxZ,GAAGqa,cAAcrZ,QAAQiI,KAAKuC,KAC7D6N,GAAaG,EAASxZ,GAAGqa,cAAcxS,KAAKoB,KAAKuC,KANjDgO,EAASxZ,GAAGqa,aAAe,gBAAkBjB,KAC7CC,GAAaG,EAASxZ,GAAGqa,eAAkBpR,KAAKuC,IAC5ChG,EAAcQ,gBAAiB,GACjCsU,GAAkBd,EAASxZ,IAK/B,IAAIua,GAAkBpB,GAAYlQ,KAAKuC,KAAO2N,GAAYlQ,KAAKuC,IAAIgO,QACtB,MAAzCe,EAAgBvZ,QAAQwY,EAASxZ,KACnCua,EAAgB1S,KAAK2R,EAASxZ,IAIpC,MAAOiJ,OAMLuR,GAAgB,SAAShB,GAC3B,GAAIK,GAAOV,GAAYlQ,KAAKuC,GAC5B,KAAKqO,EACH,MAAO5Q,KAET,IACIwR,GADAF,EAAkBV,EAAKL,QAGzBA,GADsB,mBAAbA,GACEe,EAAgBtb,MAAM,GAEtBmb,GAAUZ,EAEvB,KAAK,GAAIxZ,GAAIwZ,EAAS/Y,OAAQT,KAC5B,GAAIpB,EAAQkB,KAAK0Z,EAAUxZ,IAAMwZ,EAASxZ,IAA+B,IAAzBwZ,EAASxZ,GAAGN,SAAgB,CAE1E,IADA+a,EAAa,EAC8D,MAAnEA,EAAaF,EAAgBvZ,QAAQwY,EAASxZ,GAAIya,KACxDF,EAAgBhS,OAAOkS,EAAY,EAErC,IAAIC,GAAYrB,GAAaG,EAASxZ,GAAGqa,aACzC,IAAIK,EAAW,CAEb,IADAD,EAAa,EACoD,MAAzDA,EAAaC,EAAU1Z,QAAQiI,KAAKuC,GAAIiP,KAC9CC,EAAUnS,OAAOkS,EAAY,EAEN,KAArBC,EAAUja,SACR+E,EAAcQ,gBAAiB,GACjC2U,GAAqBnB,EAASxZ,UAEzBwZ,GAASxZ,GAAGqa,eAK3B,MAAOpR,OAML2R,GAAkB,WACpB,GAAIf,GAAOV,GAAYlQ,KAAKuC,GAC5B,OAAOqO,IAAQA,EAAKL,SAAWK,EAAKL,SAASva,MAAM,OAMjD4b,GAAiB,WACd1B,GAAYlQ,KAAKuC,MAGtBvC,KAAK6R,SACL7R,KAAKX,YACE6Q,IAAYlQ,KAAKuC,MAMtByO,GAAoB,SAASvR,GAC/B,IAAMA,IAASA,EAAMX,KACnB,OAAO,CAET,IAAIW,EAAM+Q,QAAU/Q,EAAM+Q,SAAWxQ,KACnC,OAAO,CAET,IAAI4Q,GAAOV,GAAYlQ,KAAKuC,IACxBuP,EAAalB,GAAQA,EAAKL,SAC1BwB,IAAkBD,GAAcA,EAAWta,OAAS,EACpDwa,GAAcvS,EAAMlI,QAAUwa,GAAsD,KAArCD,EAAW/Z,QAAQ0H,EAAMlI,QACxE0a,EAAgBxS,EAAM+C,eAAiBuP,GAA6D,KAA5CD,EAAW/Z,QAAQ0H,EAAM+C,eACjF0P,EAAazS,EAAM+Q,QAAU/Q,EAAM+Q,SAAWxQ,IAClD,OAAK4Q,KAAUoB,GAAcC,GAAiBC,IAGvC,GAFE,GAUPjB,GAA2B,SAASxR,GACtC,GAAImR,GAAOV,GAAYlQ,KAAKuC,GAC5B,IAAuB,gBAAV9C,IAAsBA,GAASA,EAAMX,MAAQ8R,EAA1D,CAGA,GAAIrL,GAAQJ,GAAoB1F,GAC5BgG,EAAuBmL,GAAQA,EAAKjL,SAAS,SAC7CD,EAAuBkL,GAAQA,EAAKjL,SAASlG,EAAMX,UACnD6G,EAAWF,EAAqBG,OAAOF,EAC3C,IAAIC,GAAYA,EAASnO,OAAQ,CAC/B,GAAIT,GAAGC,EAAKqO,EAAMC,EAAS5F,EAAWmG,EAAkB7F,IACxD,KAAKjJ,EAAI,EAAGC,EAAM2O,EAASnO,OAAYR,EAAJD,EAASA,IAC1CsO,EAAOM,EAAS5O,GAChBuO,EAAUO,EACU,gBAATR,IAA8C,kBAAlB9R,GAAQ8R,KAC7CA,EAAO9R,EAAQ8R,IAEG,gBAATA,IAAqBA,GAAoC,kBAArBA,GAAKS,cAClDR,EAAUD,EACVA,EAAOA,EAAKS,aAEM,kBAATT,KACT3F,EAAY5I,KAAY2I,GACxB2F,GAAkBC,EAAMC,GAAW5F,GAAa6F,OAWpD4L,GAAY,SAASZ,GAIvB,MAHwB,gBAAbA,KACTA,MAEgC,gBAApBA,GAAS/Y,QAAwB+Y,GAAaA,GAQ1Dc,GAAoB,SAASjQ,GAC/B,GAAMA,GAAgC,IAArBA,EAAQ3K,SAAzB,CAGA,GAAI0b,GAAuB,SAAS1S,IAC5BA,IAAUA,EAAQlM,EAAQkM,UAGV,OAAlBA,EAAMyG,UACRzG,EAAM2S,2BACN3S,EAAM4S,wBAED5S,GAAMyG,UAEXoM,EAAoB,SAAS7S,IACzBA,IAAUA,EAAQlM,EAAQkM,UAGhC0S,EAAqB1S,GACrBvB,GAAc2I,MAAMzF,IAEtBA,GAAQmR,iBAAiB,YAAaD,GAAmB,GACzDlR,EAAQmR,iBAAiB,WAAYJ,GAAsB,GAC3D/Q,EAAQmR,iBAAiB,aAAcJ,GAAsB,GAC7D/Q,EAAQmR,iBAAiB,aAAcJ,GAAsB,GAC7D/Q,EAAQmR,iBAAiB,YAAaJ,GAAsB,GAC5D9B,GAAejP,EAAQgQ,eACrBoB,UAAWF,EACXG,SAAUN,EACVO,WAAYP,EACZQ,WAAYR,EACZS,UAAWT,KASXT,GAAuB,SAAStQ,GAClC,GAAMA,GAAgC,IAArBA,EAAQ3K,SAAzB,CAGA,GAAIoc,GAAgBxC,GAAejP,EAAQgQ,aAC3C,IAA+B,gBAAlByB,IAA8BA,EAA3C,CAIA,IAAK,GADDC,GAAKpF,EAAKqF,GAAgB,OAAQ,QAAS,QAAS,MAAO,QACtDhc,EAAI,EAAGC,EAAM+b,EAAYvb,OAAYR,EAAJD,EAASA,IACjD+b,EAAM,QAAUC,EAAYhc,GAC5B2W,EAAMmF,EAAcC,GACD,kBAARpF,IACTtM,EAAQ4R,oBAAoBF,EAAKpF,GAAK,SAGnC2C,IAAejP,EAAQgQ,gBAQhClT,IAAcqR,cAAgB,WAC5Be,GAAmB9K,MAAMxF,KAAMrJ,EAAMW,aAOvC4G,GAActI,UAAU+I,GAAK,WAC3B,MAAOgS,IAAUnL,MAAMxF,KAAMrJ,EAAMW,aASrC4G,GAActI,UAAUyJ,IAAM,WAC5B,MAAOwR,IAAWrL,MAAMxF,KAAMrJ,EAAMW,aAQtC4G,GAActI,UAAU+P,SAAW,WACjC,MAAOmL,IAAiBtL,MAAMxF,KAAMrJ,EAAMW,aAO5C4G,GAActI,UAAUiJ,KAAO,WAC7B,MAAOkS,IAAYvL,MAAMxF,KAAMrJ,EAAMW,aAOvC4G,GAActI,UAAU8a,KAAO,WAC7B,MAAOQ,IAAY1L,MAAMxF,KAAMrJ,EAAMW,aAQvC4G,GAActI,UAAUic,OAAS,WAC/B,MAAON,IAAc/L,MAAMxF,KAAMrJ,EAAMW,aAOzC4G,GAActI,UAAU2a,SAAW,WACjC,MAAOoB,IAAgBnM,MAAMxF,KAAMrJ,EAAMW,aAQ3C4G,GAActI,UAAUia,QAAU,WAChC,MAAO+B,IAAepM,MAAMxF,KAAMrJ,EAAMW,aAO1C4G,GAActI,UAAUqd,QAAU,SAAS9I,GACzC,IAAK+F,GAAYlQ,KAAKuC,IACpB,KAAM,IAAI5N,OAAM,yFAGlB,OADAuJ,IAAc6E,QAAQ,aAAcoH,GAC7BnK,MAOT9B,GAActI,UAAUsd,QAAU,SAAS9I,GACzC,IAAK8F,GAAYlQ,KAAKuC,IACpB,KAAM,IAAI5N,OAAM,yFAGlB,OADAuJ,IAAc6E,QAAQ,YAAaqH,GAC5BpK,MAOT9B,GAActI,UAAUud,YAAc,SAASC,GAC7C,IAAKlD,GAAYlQ,KAAKuC,IACpB,KAAM,IAAI5N,OAAM,yFAGlB,OADAuJ,IAAc6E,QAAQ,kBAAmBqQ,GAClCpT,MAOT9B,GAActI,UAAUmN,QAAU,WAChC,IAAKmN,GAAYlQ,KAAKuC,IACpB,KAAM,IAAI5N,OAAM,yFAGlB,OADAuJ,IAAc6E,QAAQyC,MAAMxF,KAAMrJ,EAAMW,YACjC0I,MAQT9B,GAActI,UAAU8K,UAAY,WAClC,IAAKwP,GAAYlQ,KAAKuC,IACpB,KAAM,IAAI5N,OAAM,2FAGlB,OADAuJ,IAAcwC,UAAU8E,MAAMxF,KAAMrJ,EAAMW,YACnC0I,MAQT9B,GAActI,UAAUka,QAAU,WAChC,IAAKI,GAAYlQ,KAAKuC,IACpB,KAAM,IAAI5N,OAAM,yFAElB,OAAOuJ,IAAc4R,QAAQtK,MAAMxF,KAAMrJ,EAAMW,aAE3B,kBAAX+b,SAAyBA,OAAOC,IACzCD,OAAO,WACL,MAAOnV,MAEkB,gBAAXqV,SAAuBA,QAAoC,gBAAnBA,QAAOC,SAAwBD,OAAOC,QAC9FD,OAAOC,QAAUtV,GAEjBhL,EAAOgL,cAAgBA,IAExB,WACD,MAAO8B,OAAQ9M","sourcesContent":["/*!\n * ZeroClipboard\n * The ZeroClipboard library provides an easy way to copy text to the clipboard using an invisible Adobe Flash movie and a JavaScript interface.\n * Copyright (c) 2009-2014 Jon Rohan, James M. Greene\n * Licensed MIT\n * http://zeroclipboard.org/\n * v2.2.0\n */\n(function(window, undefined) {\n \"use strict\";\n /**\n * Store references to critically important global functions that may be\n * overridden on certain web pages.\n */\n var _window = window, _document = _window.document, _navigator = _window.navigator, _setTimeout = _window.setTimeout, _clearTimeout = _window.clearTimeout, _setInterval = _window.setInterval, _clearInterval = _window.clearInterval, _getComputedStyle = _window.getComputedStyle, _encodeURIComponent = _window.encodeURIComponent, _ActiveXObject = _window.ActiveXObject, _Error = _window.Error, _parseInt = _window.Number.parseInt || _window.parseInt, _parseFloat = _window.Number.parseFloat || _window.parseFloat, _isNaN = _window.Number.isNaN || _window.isNaN, _now = _window.Date.now, _keys = _window.Object.keys, _defineProperty = _window.Object.defineProperty, _hasOwn = _window.Object.prototype.hasOwnProperty, _slice = _window.Array.prototype.slice, _unwrap = function() {\n var unwrapper = function(el) {\n return el;\n };\n if (typeof _window.wrap === \"function\" && typeof _window.unwrap === \"function\") {\n try {\n var div = _document.createElement(\"div\");\n var unwrappedDiv = _window.unwrap(div);\n if (div.nodeType === 1 && unwrappedDiv && unwrappedDiv.nodeType === 1) {\n unwrapper = _window.unwrap;\n }\n } catch (e) {}\n }\n return unwrapper;\n }();\n /**\n * Convert an `arguments` object into an Array.\n *\n * @returns The arguments as an Array\n * @private\n */\n var _args = function(argumentsObj) {\n return _slice.call(argumentsObj, 0);\n };\n /**\n * Shallow-copy the owned, enumerable properties of one object over to another, similar to jQuery's `$.extend`.\n *\n * @returns The target object, augmented\n * @private\n */\n var _extend = function() {\n var i, len, arg, prop, src, copy, args = _args(arguments), target = args[0] || {};\n for (i = 1, len = args.length; i < len; i++) {\n if ((arg = args[i]) != null) {\n for (prop in arg) {\n if (_hasOwn.call(arg, prop)) {\n src = target[prop];\n copy = arg[prop];\n if (target !== copy && copy !== undefined) {\n target[prop] = copy;\n }\n }\n }\n }\n }\n return target;\n };\n /**\n * Return a deep copy of the source object or array.\n *\n * @returns Object or Array\n * @private\n */\n var _deepCopy = function(source) {\n var copy, i, len, prop;\n if (typeof source !== \"object\" || source == null || typeof source.nodeType === \"number\") {\n copy = source;\n } else if (typeof source.length === \"number\") {\n copy = [];\n for (i = 0, len = source.length; i < len; i++) {\n if (_hasOwn.call(source, i)) {\n copy[i] = _deepCopy(source[i]);\n }\n }\n } else {\n copy = {};\n for (prop in source) {\n if (_hasOwn.call(source, prop)) {\n copy[prop] = _deepCopy(source[prop]);\n }\n }\n }\n return copy;\n };\n /**\n * Makes a shallow copy of `obj` (like `_extend`) but filters its properties based on a list of `keys` to keep.\n * The inverse of `_omit`, mostly. The big difference is that these properties do NOT need to be enumerable to\n * be kept.\n *\n * @returns A new filtered object.\n * @private\n */\n var _pick = function(obj, keys) {\n var newObj = {};\n for (var i = 0, len = keys.length; i < len; i++) {\n if (keys[i] in obj) {\n newObj[keys[i]] = obj[keys[i]];\n }\n }\n return newObj;\n };\n /**\n * Makes a shallow copy of `obj` (like `_extend`) but filters its properties based on a list of `keys` to omit.\n * The inverse of `_pick`.\n *\n * @returns A new filtered object.\n * @private\n */\n var _omit = function(obj, keys) {\n var newObj = {};\n for (var prop in obj) {\n if (keys.indexOf(prop) === -1) {\n newObj[prop] = obj[prop];\n }\n }\n return newObj;\n };\n /**\n * Remove all owned, enumerable properties from an object.\n *\n * @returns The original object without its owned, enumerable properties.\n * @private\n */\n var _deleteOwnProperties = function(obj) {\n if (obj) {\n for (var prop in obj) {\n if (_hasOwn.call(obj, prop)) {\n delete obj[prop];\n }\n }\n }\n return obj;\n };\n /**\n * Determine if an element is contained within another element.\n *\n * @returns Boolean\n * @private\n */\n var _containedBy = function(el, ancestorEl) {\n if (el && el.nodeType === 1 && el.ownerDocument && ancestorEl && (ancestorEl.nodeType === 1 && ancestorEl.ownerDocument && ancestorEl.ownerDocument === el.ownerDocument || ancestorEl.nodeType === 9 && !ancestorEl.ownerDocument && ancestorEl === el.ownerDocument)) {\n do {\n if (el === ancestorEl) {\n return true;\n }\n el = el.parentNode;\n } while (el);\n }\n return false;\n };\n /**\n * Get the URL path's parent directory.\n *\n * @returns String or `undefined`\n * @private\n */\n var _getDirPathOfUrl = function(url) {\n var dir;\n if (typeof url === \"string\" && url) {\n dir = url.split(\"#\")[0].split(\"?\")[0];\n dir = url.slice(0, url.lastIndexOf(\"/\") + 1);\n }\n return dir;\n };\n /**\n * Get the current script's URL by throwing an `Error` and analyzing it.\n *\n * @returns String or `undefined`\n * @private\n */\n var _getCurrentScriptUrlFromErrorStack = function(stack) {\n var url, matches;\n if (typeof stack === \"string\" && stack) {\n matches = stack.match(/^(?:|[^:@]*@|.+\\)@(?=http[s]?|file)|.+?\\s+(?: at |@)(?:[^:\\(]+ )*[\\(]?)((?:http[s]?|file):\\/\\/[\\/]?.+?\\/[^:\\)]*?)(?::\\d+)(?::\\d+)?/);\n if (matches && matches[1]) {\n url = matches[1];\n } else {\n matches = stack.match(/\\)@((?:http[s]?|file):\\/\\/[\\/]?.+?\\/[^:\\)]*?)(?::\\d+)(?::\\d+)?/);\n if (matches && matches[1]) {\n url = matches[1];\n }\n }\n }\n return url;\n };\n /**\n * Get the current script's URL by throwing an `Error` and analyzing it.\n *\n * @returns String or `undefined`\n * @private\n */\n var _getCurrentScriptUrlFromError = function() {\n var url, err;\n try {\n throw new _Error();\n } catch (e) {\n err = e;\n }\n if (err) {\n url = err.sourceURL || err.fileName || _getCurrentScriptUrlFromErrorStack(err.stack);\n }\n return url;\n };\n /**\n * Get the current script's URL.\n *\n * @returns String or `undefined`\n * @private\n */\n var _getCurrentScriptUrl = function() {\n var jsPath, scripts, i;\n if (_document.currentScript && (jsPath = _document.currentScript.src)) {\n return jsPath;\n }\n scripts = _document.getElementsByTagName(\"script\");\n if (scripts.length === 1) {\n return scripts[0].src || undefined;\n }\n if (\"readyState\" in scripts[0]) {\n for (i = scripts.length; i--; ) {\n if (scripts[i].readyState === \"interactive\" && (jsPath = scripts[i].src)) {\n return jsPath;\n }\n }\n }\n if (_document.readyState === \"loading\" && (jsPath = scripts[scripts.length - 1].src)) {\n return jsPath;\n }\n if (jsPath = _getCurrentScriptUrlFromError()) {\n return jsPath;\n }\n return undefined;\n };\n /**\n * Get the unanimous parent directory of ALL script tags.\n * If any script tags are either (a) inline or (b) from differing parent\n * directories, this method must return `undefined`.\n *\n * @returns String or `undefined`\n * @private\n */\n var _getUnanimousScriptParentDir = function() {\n var i, jsDir, jsPath, scripts = _document.getElementsByTagName(\"script\");\n for (i = scripts.length; i--; ) {\n if (!(jsPath = scripts[i].src)) {\n jsDir = null;\n break;\n }\n jsPath = _getDirPathOfUrl(jsPath);\n if (jsDir == null) {\n jsDir = jsPath;\n } else if (jsDir !== jsPath) {\n jsDir = null;\n break;\n }\n }\n return jsDir || undefined;\n };\n /**\n * Get the presumed location of the \"ZeroClipboard.swf\" file, based on the location\n * of the executing JavaScript file (e.g. \"ZeroClipboard.js\", etc.).\n *\n * @returns String\n * @private\n */\n var _getDefaultSwfPath = function() {\n var jsDir = _getDirPathOfUrl(_getCurrentScriptUrl()) || _getUnanimousScriptParentDir() || \"\";\n return jsDir + \"ZeroClipboard.swf\";\n };\n /**\n * Keep track of if the page is framed (in an `iframe`). This can never change.\n * @private\n */\n var _pageIsFramed = function() {\n return window.opener == null && (!!window.top && window != window.top || !!window.parent && window != window.parent);\n }();\n /**\n * Keep track of the state of the Flash object.\n * @private\n */\n var _flashState = {\n bridge: null,\n version: \"0.0.0\",\n pluginType: \"unknown\",\n disabled: null,\n outdated: null,\n sandboxed: null,\n unavailable: null,\n degraded: null,\n deactivated: null,\n overdue: null,\n ready: null\n };\n /**\n * The minimum Flash Player version required to use ZeroClipboard completely.\n * @readonly\n * @private\n */\n var _minimumFlashVersion = \"11.0.0\";\n /**\n * The ZeroClipboard library version number, as reported by Flash, at the time the SWF was compiled.\n */\n var _zcSwfVersion;\n /**\n * Keep track of all event listener registrations.\n * @private\n */\n var _handlers = {};\n /**\n * Keep track of the currently activated element.\n * @private\n */\n var _currentElement;\n /**\n * Keep track of the element that was activated when a `copy` process started.\n * @private\n */\n var _copyTarget;\n /**\n * Keep track of data for the pending clipboard transaction.\n * @private\n */\n var _clipData = {};\n /**\n * Keep track of data formats for the pending clipboard transaction.\n * @private\n */\n var _clipDataFormatMap = null;\n /**\n * Keep track of the Flash availability check timeout.\n * @private\n */\n var _flashCheckTimeout = 0;\n /**\n * Keep track of SWF network errors interval polling.\n * @private\n */\n var _swfFallbackCheckInterval = 0;\n /**\n * The `message` store for events\n * @private\n */\n var _eventMessages = {\n ready: \"Flash communication is established\",\n error: {\n \"flash-disabled\": \"Flash is disabled or not installed. May also be attempting to run Flash in a sandboxed iframe, which is impossible.\",\n \"flash-outdated\": \"Flash is too outdated to support ZeroClipboard\",\n \"flash-sandboxed\": \"Attempting to run Flash in a sandboxed iframe, which is impossible\",\n \"flash-unavailable\": \"Flash is unable to communicate bidirectionally with JavaScript\",\n \"flash-degraded\": \"Flash is unable to preserve data fidelity when communicating with JavaScript\",\n \"flash-deactivated\": \"Flash is too outdated for your browser and/or is configured as click-to-activate.\\nThis may also mean that the ZeroClipboard SWF object could not be loaded, so please check your `swfPath` configuration and/or network connectivity.\\nMay also be attempting to run Flash in a sandboxed iframe, which is impossible.\",\n \"flash-overdue\": \"Flash communication was established but NOT within the acceptable time limit\",\n \"version-mismatch\": \"ZeroClipboard JS version number does not match ZeroClipboard SWF version number\",\n \"clipboard-error\": \"At least one error was thrown while ZeroClipboard was attempting to inject your data into the clipboard\",\n \"config-mismatch\": \"ZeroClipboard configuration does not match Flash's reality\",\n \"swf-not-found\": \"The ZeroClipboard SWF object could not be loaded, so please check your `swfPath` configuration and/or network connectivity\"\n }\n };\n /**\n * The `name`s of `error` events that can only occur is Flash has at least\n * been able to load the SWF successfully.\n * @private\n */\n var _errorsThatOnlyOccurAfterFlashLoads = [ \"flash-unavailable\", \"flash-degraded\", \"flash-overdue\", \"version-mismatch\", \"config-mismatch\", \"clipboard-error\" ];\n /**\n * The `name`s of `error` events that should likely result in the `_flashState`\n * variable's property values being updated.\n * @private\n */\n var _flashStateErrorNames = [ \"flash-disabled\", \"flash-outdated\", \"flash-sandboxed\", \"flash-unavailable\", \"flash-degraded\", \"flash-deactivated\", \"flash-overdue\" ];\n /**\n * A RegExp to match the `name` property of `error` events related to Flash.\n * @private\n */\n var _flashStateErrorNameMatchingRegex = new RegExp(\"^flash-(\" + _flashStateErrorNames.map(function(errorName) {\n return errorName.replace(/^flash-/, \"\");\n }).join(\"|\") + \")$\");\n /**\n * A RegExp to match the `name` property of `error` events related to Flash,\n * which is enabled.\n * @private\n */\n var _flashStateEnabledErrorNameMatchingRegex = new RegExp(\"^flash-(\" + _flashStateErrorNames.slice(1).map(function(errorName) {\n return errorName.replace(/^flash-/, \"\");\n }).join(\"|\") + \")$\");\n /**\n * ZeroClipboard configuration defaults for the Core module.\n * @private\n */\n var _globalConfig = {\n swfPath: _getDefaultSwfPath(),\n trustedDomains: window.location.host ? [ window.location.host ] : [],\n cacheBust: true,\n forceEnhancedClipboard: false,\n flashLoadTimeout: 3e4,\n autoActivate: true,\n bubbleEvents: true,\n containerId: \"global-zeroclipboard-html-bridge\",\n containerClass: \"global-zeroclipboard-container\",\n swfObjectId: \"global-zeroclipboard-flash-bridge\",\n hoverClass: \"zeroclipboard-is-hover\",\n activeClass: \"zeroclipboard-is-active\",\n forceHandCursor: false,\n title: null,\n zIndex: 999999999\n };\n /**\n * The underlying implementation of `ZeroClipboard.config`.\n * @private\n */\n var _config = function(options) {\n if (typeof options === \"object\" && options !== null) {\n for (var prop in options) {\n if (_hasOwn.call(options, prop)) {\n if (/^(?:forceHandCursor|title|zIndex|bubbleEvents)$/.test(prop)) {\n _globalConfig[prop] = options[prop];\n } else if (_flashState.bridge == null) {\n if (prop === \"containerId\" || prop === \"swfObjectId\") {\n if (_isValidHtml4Id(options[prop])) {\n _globalConfig[prop] = options[prop];\n } else {\n throw new Error(\"The specified `\" + prop + \"` value is not valid as an HTML4 Element ID\");\n }\n } else {\n _globalConfig[prop] = options[prop];\n }\n }\n }\n }\n }\n if (typeof options === \"string\" && options) {\n if (_hasOwn.call(_globalConfig, options)) {\n return _globalConfig[options];\n }\n return;\n }\n return _deepCopy(_globalConfig);\n };\n /**\n * The underlying implementation of `ZeroClipboard.state`.\n * @private\n */\n var _state = function() {\n _detectSandbox();\n return {\n browser: _pick(_navigator, [ \"userAgent\", \"platform\", \"appName\" ]),\n flash: _omit(_flashState, [ \"bridge\" ]),\n zeroclipboard: {\n version: ZeroClipboard.version,\n config: ZeroClipboard.config()\n }\n };\n };\n /**\n * The underlying implementation of `ZeroClipboard.isFlashUnusable`.\n * @private\n */\n var _isFlashUnusable = function() {\n return !!(_flashState.disabled || _flashState.outdated || _flashState.sandboxed || _flashState.unavailable || _flashState.degraded || _flashState.deactivated);\n };\n /**\n * The underlying implementation of `ZeroClipboard.on`.\n * @private\n */\n var _on = function(eventType, listener) {\n var i, len, events, added = {};\n if (typeof eventType === \"string\" && eventType) {\n events = eventType.toLowerCase().split(/\\s+/);\n } else if (typeof eventType === \"object\" && eventType && typeof listener === \"undefined\") {\n for (i in eventType) {\n if (_hasOwn.call(eventType, i) && typeof i === \"string\" && i && typeof eventType[i] === \"function\") {\n ZeroClipboard.on(i, eventType[i]);\n }\n }\n }\n if (events && events.length) {\n for (i = 0, len = events.length; i < len; i++) {\n eventType = events[i].replace(/^on/, \"\");\n added[eventType] = true;\n if (!_handlers[eventType]) {\n _handlers[eventType] = [];\n }\n _handlers[eventType].push(listener);\n }\n if (added.ready && _flashState.ready) {\n ZeroClipboard.emit({\n type: \"ready\"\n });\n }\n if (added.error) {\n for (i = 0, len = _flashStateErrorNames.length; i < len; i++) {\n if (_flashState[_flashStateErrorNames[i].replace(/^flash-/, \"\")] === true) {\n ZeroClipboard.emit({\n type: \"error\",\n name: _flashStateErrorNames[i]\n });\n break;\n }\n }\n if (_zcSwfVersion !== undefined && ZeroClipboard.version !== _zcSwfVersion) {\n ZeroClipboard.emit({\n type: \"error\",\n name: \"version-mismatch\",\n jsVersion: ZeroClipboard.version,\n swfVersion: _zcSwfVersion\n });\n }\n }\n }\n return ZeroClipboard;\n };\n /**\n * The underlying implementation of `ZeroClipboard.off`.\n * @private\n */\n var _off = function(eventType, listener) {\n var i, len, foundIndex, events, perEventHandlers;\n if (arguments.length === 0) {\n events = _keys(_handlers);\n } else if (typeof eventType === \"string\" && eventType) {\n events = eventType.split(/\\s+/);\n } else if (typeof eventType === \"object\" && eventType && typeof listener === \"undefined\") {\n for (i in eventType) {\n if (_hasOwn.call(eventType, i) && typeof i === \"string\" && i && typeof eventType[i] === \"function\") {\n ZeroClipboard.off(i, eventType[i]);\n }\n }\n }\n if (events && events.length) {\n for (i = 0, len = events.length; i < len; i++) {\n eventType = events[i].toLowerCase().replace(/^on/, \"\");\n perEventHandlers = _handlers[eventType];\n if (perEventHandlers && perEventHandlers.length) {\n if (listener) {\n foundIndex = perEventHandlers.indexOf(listener);\n while (foundIndex !== -1) {\n perEventHandlers.splice(foundIndex, 1);\n foundIndex = perEventHandlers.indexOf(listener, foundIndex);\n }\n } else {\n perEventHandlers.length = 0;\n }\n }\n }\n }\n return ZeroClipboard;\n };\n /**\n * The underlying implementation of `ZeroClipboard.handlers`.\n * @private\n */\n var _listeners = function(eventType) {\n var copy;\n if (typeof eventType === \"string\" && eventType) {\n copy = _deepCopy(_handlers[eventType]) || null;\n } else {\n copy = _deepCopy(_handlers);\n }\n return copy;\n };\n /**\n * The underlying implementation of `ZeroClipboard.emit`.\n * @private\n */\n var _emit = function(event) {\n var eventCopy, returnVal, tmp;\n event = _createEvent(event);\n if (!event) {\n return;\n }\n if (_preprocessEvent(event)) {\n return;\n }\n if (event.type === \"ready\" && _flashState.overdue === true) {\n return ZeroClipboard.emit({\n type: \"error\",\n name: \"flash-overdue\"\n });\n }\n eventCopy = _extend({}, event);\n _dispatchCallbacks.call(this, eventCopy);\n if (event.type === \"copy\") {\n tmp = _mapClipDataToFlash(_clipData);\n returnVal = tmp.data;\n _clipDataFormatMap = tmp.formatMap;\n }\n return returnVal;\n };\n /**\n * The underlying implementation of `ZeroClipboard.create`.\n * @private\n */\n var _create = function() {\n var previousState = _flashState.sandboxed;\n _detectSandbox();\n if (typeof _flashState.ready !== \"boolean\") {\n _flashState.ready = false;\n }\n if (_flashState.sandboxed !== previousState && _flashState.sandboxed === true) {\n _flashState.ready = false;\n ZeroClipboard.emit({\n type: \"error\",\n name: \"flash-sandboxed\"\n });\n } else if (!ZeroClipboard.isFlashUnusable() && _flashState.bridge === null) {\n var maxWait = _globalConfig.flashLoadTimeout;\n if (typeof maxWait === \"number\" && maxWait >= 0) {\n _flashCheckTimeout = _setTimeout(function() {\n if (typeof _flashState.deactivated !== \"boolean\") {\n _flashState.deactivated = true;\n }\n if (_flashState.deactivated === true) {\n ZeroClipboard.emit({\n type: \"error\",\n name: \"flash-deactivated\"\n });\n }\n }, maxWait);\n }\n _flashState.overdue = false;\n _embedSwf();\n }\n };\n /**\n * The underlying implementation of `ZeroClipboard.destroy`.\n * @private\n */\n var _destroy = function() {\n ZeroClipboard.clearData();\n ZeroClipboard.blur();\n ZeroClipboard.emit(\"destroy\");\n _unembedSwf();\n ZeroClipboard.off();\n };\n /**\n * The underlying implementation of `ZeroClipboard.setData`.\n * @private\n */\n var _setData = function(format, data) {\n var dataObj;\n if (typeof format === \"object\" && format && typeof data === \"undefined\") {\n dataObj = format;\n ZeroClipboard.clearData();\n } else if (typeof format === \"string\" && format) {\n dataObj = {};\n dataObj[format] = data;\n } else {\n return;\n }\n for (var dataFormat in dataObj) {\n if (typeof dataFormat === \"string\" && dataFormat && _hasOwn.call(dataObj, dataFormat) && typeof dataObj[dataFormat] === \"string\" && dataObj[dataFormat]) {\n _clipData[dataFormat] = dataObj[dataFormat];\n }\n }\n };\n /**\n * The underlying implementation of `ZeroClipboard.clearData`.\n * @private\n */\n var _clearData = function(format) {\n if (typeof format === \"undefined\") {\n _deleteOwnProperties(_clipData);\n _clipDataFormatMap = null;\n } else if (typeof format === \"string\" && _hasOwn.call(_clipData, format)) {\n delete _clipData[format];\n }\n };\n /**\n * The underlying implementation of `ZeroClipboard.getData`.\n * @private\n */\n var _getData = function(format) {\n if (typeof format === \"undefined\") {\n return _deepCopy(_clipData);\n } else if (typeof format === \"string\" && _hasOwn.call(_clipData, format)) {\n return _clipData[format];\n }\n };\n /**\n * The underlying implementation of `ZeroClipboard.focus`/`ZeroClipboard.activate`.\n * @private\n */\n var _focus = function(element) {\n if (!(element && element.nodeType === 1)) {\n return;\n }\n if (_currentElement) {\n _removeClass(_currentElement, _globalConfig.activeClass);\n if (_currentElement !== element) {\n _removeClass(_currentElement, _globalConfig.hoverClass);\n }\n }\n _currentElement = element;\n _addClass(element, _globalConfig.hoverClass);\n var newTitle = element.getAttribute(\"title\") || _globalConfig.title;\n if (typeof newTitle === \"string\" && newTitle) {\n var htmlBridge = _getHtmlBridge(_flashState.bridge);\n if (htmlBridge) {\n htmlBridge.setAttribute(\"title\", newTitle);\n }\n }\n var useHandCursor = _globalConfig.forceHandCursor === true || _getStyle(element, \"cursor\") === \"pointer\";\n _setHandCursor(useHandCursor);\n _reposition();\n };\n /**\n * The underlying implementation of `ZeroClipboard.blur`/`ZeroClipboard.deactivate`.\n * @private\n */\n var _blur = function() {\n var htmlBridge = _getHtmlBridge(_flashState.bridge);\n if (htmlBridge) {\n htmlBridge.removeAttribute(\"title\");\n htmlBridge.style.left = \"0px\";\n htmlBridge.style.top = \"-9999px\";\n htmlBridge.style.width = \"1px\";\n htmlBridge.style.height = \"1px\";\n }\n if (_currentElement) {\n _removeClass(_currentElement, _globalConfig.hoverClass);\n _removeClass(_currentElement, _globalConfig.activeClass);\n _currentElement = null;\n }\n };\n /**\n * The underlying implementation of `ZeroClipboard.activeElement`.\n * @private\n */\n var _activeElement = function() {\n return _currentElement || null;\n };\n /**\n * Check if a value is a valid HTML4 `ID` or `Name` token.\n * @private\n */\n var _isValidHtml4Id = function(id) {\n return typeof id === \"string\" && id && /^[A-Za-z][A-Za-z0-9_:\\-\\.]*$/.test(id);\n };\n /**\n * Create or update an `event` object, based on the `eventType`.\n * @private\n */\n var _createEvent = function(event) {\n var eventType;\n if (typeof event === \"string\" && event) {\n eventType = event;\n event = {};\n } else if (typeof event === \"object\" && event && typeof event.type === \"string\" && event.type) {\n eventType = event.type;\n }\n if (!eventType) {\n return;\n }\n eventType = eventType.toLowerCase();\n if (!event.target && (/^(copy|aftercopy|_click)$/.test(eventType) || eventType === \"error\" && event.name === \"clipboard-error\")) {\n event.target = _copyTarget;\n }\n _extend(event, {\n type: eventType,\n target: event.target || _currentElement || null,\n relatedTarget: event.relatedTarget || null,\n currentTarget: _flashState && _flashState.bridge || null,\n timeStamp: event.timeStamp || _now() || null\n });\n var msg = _eventMessages[event.type];\n if (event.type === \"error\" && event.name && msg) {\n msg = msg[event.name];\n }\n if (msg) {\n event.message = msg;\n }\n if (event.type === \"ready\") {\n _extend(event, {\n target: null,\n version: _flashState.version\n });\n }\n if (event.type === \"error\") {\n if (_flashStateErrorNameMatchingRegex.test(event.name)) {\n _extend(event, {\n target: null,\n minimumVersion: _minimumFlashVersion\n });\n }\n if (_flashStateEnabledErrorNameMatchingRegex.test(event.name)) {\n _extend(event, {\n version: _flashState.version\n });\n }\n }\n if (event.type === \"copy\") {\n event.clipboardData = {\n setData: ZeroClipboard.setData,\n clearData: ZeroClipboard.clearData\n };\n }\n if (event.type === \"aftercopy\") {\n event = _mapClipResultsFromFlash(event, _clipDataFormatMap);\n }\n if (event.target && !event.relatedTarget) {\n event.relatedTarget = _getRelatedTarget(event.target);\n }\n return _addMouseData(event);\n };\n /**\n * Get a relatedTarget from the target's `data-clipboard-target` attribute\n * @private\n */\n var _getRelatedTarget = function(targetEl) {\n var relatedTargetId = targetEl && targetEl.getAttribute && targetEl.getAttribute(\"data-clipboard-target\");\n return relatedTargetId ? _document.getElementById(relatedTargetId) : null;\n };\n /**\n * Add element and position data to `MouseEvent` instances\n * @private\n */\n var _addMouseData = function(event) {\n if (event && /^_(?:click|mouse(?:over|out|down|up|move))$/.test(event.type)) {\n var srcElement = event.target;\n var fromElement = event.type === \"_mouseover\" && event.relatedTarget ? event.relatedTarget : undefined;\n var toElement = event.type === \"_mouseout\" && event.relatedTarget ? event.relatedTarget : undefined;\n var pos = _getElementPosition(srcElement);\n var screenLeft = _window.screenLeft || _window.screenX || 0;\n var screenTop = _window.screenTop || _window.screenY || 0;\n var scrollLeft = _document.body.scrollLeft + _document.documentElement.scrollLeft;\n var scrollTop = _document.body.scrollTop + _document.documentElement.scrollTop;\n var pageX = pos.left + (typeof event._stageX === \"number\" ? event._stageX : 0);\n var pageY = pos.top + (typeof event._stageY === \"number\" ? event._stageY : 0);\n var clientX = pageX - scrollLeft;\n var clientY = pageY - scrollTop;\n var screenX = screenLeft + clientX;\n var screenY = screenTop + clientY;\n var moveX = typeof event.movementX === \"number\" ? event.movementX : 0;\n var moveY = typeof event.movementY === \"number\" ? event.movementY : 0;\n delete event._stageX;\n delete event._stageY;\n _extend(event, {\n srcElement: srcElement,\n fromElement: fromElement,\n toElement: toElement,\n screenX: screenX,\n screenY: screenY,\n pageX: pageX,\n pageY: pageY,\n clientX: clientX,\n clientY: clientY,\n x: clientX,\n y: clientY,\n movementX: moveX,\n movementY: moveY,\n offsetX: 0,\n offsetY: 0,\n layerX: 0,\n layerY: 0\n });\n }\n return event;\n };\n /**\n * Determine if an event's registered handlers should be execute synchronously or asynchronously.\n *\n * @returns {boolean}\n * @private\n */\n var _shouldPerformAsync = function(event) {\n var eventType = event && typeof event.type === \"string\" && event.type || \"\";\n return !/^(?:(?:before)?copy|destroy)$/.test(eventType);\n };\n /**\n * Control if a callback should be executed asynchronously or not.\n *\n * @returns `undefined`\n * @private\n */\n var _dispatchCallback = function(func, context, args, async) {\n if (async) {\n _setTimeout(function() {\n func.apply(context, args);\n }, 0);\n } else {\n func.apply(context, args);\n }\n };\n /**\n * Handle the actual dispatching of events to client instances.\n *\n * @returns `undefined`\n * @private\n */\n var _dispatchCallbacks = function(event) {\n if (!(typeof event === \"object\" && event && event.type)) {\n return;\n }\n var async = _shouldPerformAsync(event);\n var wildcardTypeHandlers = _handlers[\"*\"] || [];\n var specificTypeHandlers = _handlers[event.type] || [];\n var handlers = wildcardTypeHandlers.concat(specificTypeHandlers);\n if (handlers && handlers.length) {\n var i, len, func, context, eventCopy, originalContext = this;\n for (i = 0, len = handlers.length; i < len; i++) {\n func = handlers[i];\n context = originalContext;\n if (typeof func === \"string\" && typeof _window[func] === \"function\") {\n func = _window[func];\n }\n if (typeof func === \"object\" && func && typeof func.handleEvent === \"function\") {\n context = func;\n func = func.handleEvent;\n }\n if (typeof func === \"function\") {\n eventCopy = _extend({}, event);\n _dispatchCallback(func, context, [ eventCopy ], async);\n }\n }\n }\n return this;\n };\n /**\n * Check an `error` event's `name` property to see if Flash has\n * already loaded, which rules out possible `iframe` sandboxing.\n * @private\n */\n var _getSandboxStatusFromErrorEvent = function(event) {\n var isSandboxed = null;\n if (_pageIsFramed === false || event && event.type === \"error\" && event.name && _errorsThatOnlyOccurAfterFlashLoads.indexOf(event.name) !== -1) {\n isSandboxed = false;\n }\n return isSandboxed;\n };\n /**\n * Preprocess any special behaviors, reactions, or state changes after receiving this event.\n * Executes only once per event emitted, NOT once per client.\n * @private\n */\n var _preprocessEvent = function(event) {\n var element = event.target || _currentElement || null;\n var sourceIsSwf = event._source === \"swf\";\n delete event._source;\n switch (event.type) {\n case \"error\":\n var isSandboxed = event.name === \"flash-sandboxed\" || _getSandboxStatusFromErrorEvent(event);\n if (typeof isSandboxed === \"boolean\") {\n _flashState.sandboxed = isSandboxed;\n }\n if (_flashStateErrorNames.indexOf(event.name) !== -1) {\n _extend(_flashState, {\n disabled: event.name === \"flash-disabled\",\n outdated: event.name === \"flash-outdated\",\n unavailable: event.name === \"flash-unavailable\",\n degraded: event.name === \"flash-degraded\",\n deactivated: event.name === \"flash-deactivated\",\n overdue: event.name === \"flash-overdue\",\n ready: false\n });\n } else if (event.name === \"version-mismatch\") {\n _zcSwfVersion = event.swfVersion;\n _extend(_flashState, {\n disabled: false,\n outdated: false,\n unavailable: false,\n degraded: false,\n deactivated: false,\n overdue: false,\n ready: false\n });\n }\n _clearTimeoutsAndPolling();\n break;\n\n case \"ready\":\n _zcSwfVersion = event.swfVersion;\n var wasDeactivated = _flashState.deactivated === true;\n _extend(_flashState, {\n disabled: false,\n outdated: false,\n sandboxed: false,\n unavailable: false,\n degraded: false,\n deactivated: false,\n overdue: wasDeactivated,\n ready: !wasDeactivated\n });\n _clearTimeoutsAndPolling();\n break;\n\n case \"beforecopy\":\n _copyTarget = element;\n break;\n\n case \"copy\":\n var textContent, htmlContent, targetEl = event.relatedTarget;\n if (!(_clipData[\"text/html\"] || _clipData[\"text/plain\"]) && targetEl && (htmlContent = targetEl.value || targetEl.outerHTML || targetEl.innerHTML) && (textContent = targetEl.value || targetEl.textContent || targetEl.innerText)) {\n event.clipboardData.clearData();\n event.clipboardData.setData(\"text/plain\", textContent);\n if (htmlContent !== textContent) {\n event.clipboardData.setData(\"text/html\", htmlContent);\n }\n } else if (!_clipData[\"text/plain\"] && event.target && (textContent = event.target.getAttribute(\"data-clipboard-text\"))) {\n event.clipboardData.clearData();\n event.clipboardData.setData(\"text/plain\", textContent);\n }\n break;\n\n case \"aftercopy\":\n _queueEmitClipboardErrors(event);\n ZeroClipboard.clearData();\n if (element && element !== _safeActiveElement() && element.focus) {\n element.focus();\n }\n break;\n\n case \"_mouseover\":\n ZeroClipboard.focus(element);\n if (_globalConfig.bubbleEvents === true && sourceIsSwf) {\n if (element && element !== event.relatedTarget && !_containedBy(event.relatedTarget, element)) {\n _fireMouseEvent(_extend({}, event, {\n type: \"mouseenter\",\n bubbles: false,\n cancelable: false\n }));\n }\n _fireMouseEvent(_extend({}, event, {\n type: \"mouseover\"\n }));\n }\n break;\n\n case \"_mouseout\":\n ZeroClipboard.blur();\n if (_globalConfig.bubbleEvents === true && sourceIsSwf) {\n if (element && element !== event.relatedTarget && !_containedBy(event.relatedTarget, element)) {\n _fireMouseEvent(_extend({}, event, {\n type: \"mouseleave\",\n bubbles: false,\n cancelable: false\n }));\n }\n _fireMouseEvent(_extend({}, event, {\n type: \"mouseout\"\n }));\n }\n break;\n\n case \"_mousedown\":\n _addClass(element, _globalConfig.activeClass);\n if (_globalConfig.bubbleEvents === true && sourceIsSwf) {\n _fireMouseEvent(_extend({}, event, {\n type: event.type.slice(1)\n }));\n }\n break;\n\n case \"_mouseup\":\n _removeClass(element, _globalConfig.activeClass);\n if (_globalConfig.bubbleEvents === true && sourceIsSwf) {\n _fireMouseEvent(_extend({}, event, {\n type: event.type.slice(1)\n }));\n }\n break;\n\n case \"_click\":\n _copyTarget = null;\n if (_globalConfig.bubbleEvents === true && sourceIsSwf) {\n _fireMouseEvent(_extend({}, event, {\n type: event.type.slice(1)\n }));\n }\n break;\n\n case \"_mousemove\":\n if (_globalConfig.bubbleEvents === true && sourceIsSwf) {\n _fireMouseEvent(_extend({}, event, {\n type: event.type.slice(1)\n }));\n }\n break;\n }\n if (/^_(?:click|mouse(?:over|out|down|up|move))$/.test(event.type)) {\n return true;\n }\n };\n /**\n * Check an \"aftercopy\" event for clipboard errors and emit a corresponding \"error\" event.\n * @private\n */\n var _queueEmitClipboardErrors = function(aftercopyEvent) {\n if (aftercopyEvent.errors && aftercopyEvent.errors.length > 0) {\n var errorEvent = _deepCopy(aftercopyEvent);\n _extend(errorEvent, {\n type: \"error\",\n name: \"clipboard-error\"\n });\n delete errorEvent.success;\n _setTimeout(function() {\n ZeroClipboard.emit(errorEvent);\n }, 0);\n }\n };\n /**\n * Dispatch a synthetic MouseEvent.\n *\n * @returns `undefined`\n * @private\n */\n var _fireMouseEvent = function(event) {\n if (!(event && typeof event.type === \"string\" && event)) {\n return;\n }\n var e, target = event.target || null, doc = target && target.ownerDocument || _document, defaults = {\n view: doc.defaultView || _window,\n canBubble: true,\n cancelable: true,\n detail: event.type === \"click\" ? 1 : 0,\n button: typeof event.which === \"number\" ? event.which - 1 : typeof event.button === \"number\" ? event.button : doc.createEvent ? 0 : 1\n }, args = _extend(defaults, event);\n if (!target) {\n return;\n }\n if (doc.createEvent && target.dispatchEvent) {\n args = [ args.type, args.canBubble, args.cancelable, args.view, args.detail, args.screenX, args.screenY, args.clientX, args.clientY, args.ctrlKey, args.altKey, args.shiftKey, args.metaKey, args.button, args.relatedTarget ];\n e = doc.createEvent(\"MouseEvents\");\n if (e.initMouseEvent) {\n e.initMouseEvent.apply(e, args);\n e._source = \"js\";\n target.dispatchEvent(e);\n }\n }\n };\n /**\n * Continuously poll the DOM until either:\n * (a) the fallback content becomes visible, or\n * (b) we receive an event from SWF (handled elsewhere)\n *\n * IMPORTANT:\n * This is NOT a necessary check but it can result in significantly faster\n * detection of bad `swfPath` configuration and/or network/server issues [in\n * supported browsers] than waiting for the entire `flashLoadTimeout` duration\n * to elapse before detecting that the SWF cannot be loaded. The detection\n * duration can be anywhere from 10-30 times faster [in supported browsers] by\n * using this approach.\n *\n * @returns `undefined`\n * @private\n */\n var _watchForSwfFallbackContent = function() {\n var maxWait = _globalConfig.flashLoadTimeout;\n if (typeof maxWait === \"number\" && maxWait >= 0) {\n var pollWait = Math.min(1e3, maxWait / 10);\n var fallbackContentId = _globalConfig.swfObjectId + \"_fallbackContent\";\n _swfFallbackCheckInterval = _setInterval(function() {\n var el = _document.getElementById(fallbackContentId);\n if (_isElementVisible(el)) {\n _clearTimeoutsAndPolling();\n _flashState.deactivated = null;\n ZeroClipboard.emit({\n type: \"error\",\n name: \"swf-not-found\"\n });\n }\n }, pollWait);\n }\n };\n /**\n * Create the HTML bridge element to embed the Flash object into.\n * @private\n */\n var _createHtmlBridge = function() {\n var container = _document.createElement(\"div\");\n container.id = _globalConfig.containerId;\n container.className = _globalConfig.containerClass;\n container.style.position = \"absolute\";\n container.style.left = \"0px\";\n container.style.top = \"-9999px\";\n container.style.width = \"1px\";\n container.style.height = \"1px\";\n container.style.zIndex = \"\" + _getSafeZIndex(_globalConfig.zIndex);\n return container;\n };\n /**\n * Get the HTML element container that wraps the Flash bridge object/element.\n * @private\n */\n var _getHtmlBridge = function(flashBridge) {\n var htmlBridge = flashBridge && flashBridge.parentNode;\n while (htmlBridge && htmlBridge.nodeName === \"OBJECT\" && htmlBridge.parentNode) {\n htmlBridge = htmlBridge.parentNode;\n }\n return htmlBridge || null;\n };\n /**\n * Create the SWF object.\n *\n * @returns The SWF object reference.\n * @private\n */\n var _embedSwf = function() {\n var len, flashBridge = _flashState.bridge, container = _getHtmlBridge(flashBridge);\n if (!flashBridge) {\n var allowScriptAccess = _determineScriptAccess(_window.location.host, _globalConfig);\n var allowNetworking = allowScriptAccess === \"never\" ? \"none\" : \"all\";\n var flashvars = _vars(_extend({\n jsVersion: ZeroClipboard.version\n }, _globalConfig));\n var swfUrl = _globalConfig.swfPath + _cacheBust(_globalConfig.swfPath, _globalConfig);\n container = _createHtmlBridge();\n var divToBeReplaced = _document.createElement(\"div\");\n container.appendChild(divToBeReplaced);\n _document.body.appendChild(container);\n var tmpDiv = _document.createElement(\"div\");\n var usingActiveX = _flashState.pluginType === \"activex\";\n tmpDiv.innerHTML = '\" + (usingActiveX ? '' : \"\") + '' + '' + '' + '' + '' + '
 
' + \"
\";\n flashBridge = tmpDiv.firstChild;\n tmpDiv = null;\n _unwrap(flashBridge).ZeroClipboard = ZeroClipboard;\n container.replaceChild(flashBridge, divToBeReplaced);\n _watchForSwfFallbackContent();\n }\n if (!flashBridge) {\n flashBridge = _document[_globalConfig.swfObjectId];\n if (flashBridge && (len = flashBridge.length)) {\n flashBridge = flashBridge[len - 1];\n }\n if (!flashBridge && container) {\n flashBridge = container.firstChild;\n }\n }\n _flashState.bridge = flashBridge || null;\n return flashBridge;\n };\n /**\n * Destroy the SWF object.\n * @private\n */\n var _unembedSwf = function() {\n var flashBridge = _flashState.bridge;\n if (flashBridge) {\n var htmlBridge = _getHtmlBridge(flashBridge);\n if (htmlBridge) {\n if (_flashState.pluginType === \"activex\" && \"readyState\" in flashBridge) {\n flashBridge.style.display = \"none\";\n (function removeSwfFromIE() {\n if (flashBridge.readyState === 4) {\n for (var prop in flashBridge) {\n if (typeof flashBridge[prop] === \"function\") {\n flashBridge[prop] = null;\n }\n }\n if (flashBridge.parentNode) {\n flashBridge.parentNode.removeChild(flashBridge);\n }\n if (htmlBridge.parentNode) {\n htmlBridge.parentNode.removeChild(htmlBridge);\n }\n } else {\n _setTimeout(removeSwfFromIE, 10);\n }\n })();\n } else {\n if (flashBridge.parentNode) {\n flashBridge.parentNode.removeChild(flashBridge);\n }\n if (htmlBridge.parentNode) {\n htmlBridge.parentNode.removeChild(htmlBridge);\n }\n }\n }\n _clearTimeoutsAndPolling();\n _flashState.ready = null;\n _flashState.bridge = null;\n _flashState.deactivated = null;\n _zcSwfVersion = undefined;\n }\n };\n /**\n * Map the data format names of the \"clipData\" to Flash-friendly names.\n *\n * @returns A new transformed object.\n * @private\n */\n var _mapClipDataToFlash = function(clipData) {\n var newClipData = {}, formatMap = {};\n if (!(typeof clipData === \"object\" && clipData)) {\n return;\n }\n for (var dataFormat in clipData) {\n if (dataFormat && _hasOwn.call(clipData, dataFormat) && typeof clipData[dataFormat] === \"string\" && clipData[dataFormat]) {\n switch (dataFormat.toLowerCase()) {\n case \"text/plain\":\n case \"text\":\n case \"air:text\":\n case \"flash:text\":\n newClipData.text = clipData[dataFormat];\n formatMap.text = dataFormat;\n break;\n\n case \"text/html\":\n case \"html\":\n case \"air:html\":\n case \"flash:html\":\n newClipData.html = clipData[dataFormat];\n formatMap.html = dataFormat;\n break;\n\n case \"application/rtf\":\n case \"text/rtf\":\n case \"rtf\":\n case \"richtext\":\n case \"air:rtf\":\n case \"flash:rtf\":\n newClipData.rtf = clipData[dataFormat];\n formatMap.rtf = dataFormat;\n break;\n\n default:\n break;\n }\n }\n }\n return {\n data: newClipData,\n formatMap: formatMap\n };\n };\n /**\n * Map the data format names from Flash-friendly names back to their original \"clipData\" names (via a format mapping).\n *\n * @returns A new transformed object.\n * @private\n */\n var _mapClipResultsFromFlash = function(clipResults, formatMap) {\n if (!(typeof clipResults === \"object\" && clipResults && typeof formatMap === \"object\" && formatMap)) {\n return clipResults;\n }\n var newResults = {};\n for (var prop in clipResults) {\n if (_hasOwn.call(clipResults, prop)) {\n if (prop === \"errors\") {\n newResults[prop] = clipResults[prop] ? clipResults[prop].slice() : [];\n for (var i = 0, len = newResults[prop].length; i < len; i++) {\n newResults[prop][i].format = formatMap[newResults[prop][i].format];\n }\n } else if (prop !== \"success\" && prop !== \"data\") {\n newResults[prop] = clipResults[prop];\n } else {\n newResults[prop] = {};\n var tmpHash = clipResults[prop];\n for (var dataFormat in tmpHash) {\n if (dataFormat && _hasOwn.call(tmpHash, dataFormat) && _hasOwn.call(formatMap, dataFormat)) {\n newResults[prop][formatMap[dataFormat]] = tmpHash[dataFormat];\n }\n }\n }\n }\n }\n return newResults;\n };\n /**\n * Will look at a path, and will create a \"?noCache={time}\" or \"&noCache={time}\"\n * query param string to return. Does NOT append that string to the original path.\n * This is useful because ExternalInterface often breaks when a Flash SWF is cached.\n *\n * @returns The `noCache` query param with necessary \"?\"/\"&\" prefix.\n * @private\n */\n var _cacheBust = function(path, options) {\n var cacheBust = options == null || options && options.cacheBust === true;\n if (cacheBust) {\n return (path.indexOf(\"?\") === -1 ? \"?\" : \"&\") + \"noCache=\" + _now();\n } else {\n return \"\";\n }\n };\n /**\n * Creates a query string for the FlashVars param.\n * Does NOT include the cache-busting query param.\n *\n * @returns FlashVars query string\n * @private\n */\n var _vars = function(options) {\n var i, len, domain, domains, str = \"\", trustedOriginsExpanded = [];\n if (options.trustedDomains) {\n if (typeof options.trustedDomains === \"string\") {\n domains = [ options.trustedDomains ];\n } else if (typeof options.trustedDomains === \"object\" && \"length\" in options.trustedDomains) {\n domains = options.trustedDomains;\n }\n }\n if (domains && domains.length) {\n for (i = 0, len = domains.length; i < len; i++) {\n if (_hasOwn.call(domains, i) && domains[i] && typeof domains[i] === \"string\") {\n domain = _extractDomain(domains[i]);\n if (!domain) {\n continue;\n }\n if (domain === \"*\") {\n trustedOriginsExpanded.length = 0;\n trustedOriginsExpanded.push(domain);\n break;\n }\n trustedOriginsExpanded.push.apply(trustedOriginsExpanded, [ domain, \"//\" + domain, _window.location.protocol + \"//\" + domain ]);\n }\n }\n }\n if (trustedOriginsExpanded.length) {\n str += \"trustedOrigins=\" + _encodeURIComponent(trustedOriginsExpanded.join(\",\"));\n }\n if (options.forceEnhancedClipboard === true) {\n str += (str ? \"&\" : \"\") + \"forceEnhancedClipboard=true\";\n }\n if (typeof options.swfObjectId === \"string\" && options.swfObjectId) {\n str += (str ? \"&\" : \"\") + \"swfObjectId=\" + _encodeURIComponent(options.swfObjectId);\n }\n if (typeof options.jsVersion === \"string\" && options.jsVersion) {\n str += (str ? \"&\" : \"\") + \"jsVersion=\" + _encodeURIComponent(options.jsVersion);\n }\n return str;\n };\n /**\n * Extract the domain (e.g. \"github.com\") from an origin (e.g. \"https://github.com\") or\n * URL (e.g. \"https://github.com/zeroclipboard/zeroclipboard/\").\n *\n * @returns the domain\n * @private\n */\n var _extractDomain = function(originOrUrl) {\n if (originOrUrl == null || originOrUrl === \"\") {\n return null;\n }\n originOrUrl = originOrUrl.replace(/^\\s+|\\s+$/g, \"\");\n if (originOrUrl === \"\") {\n return null;\n }\n var protocolIndex = originOrUrl.indexOf(\"//\");\n originOrUrl = protocolIndex === -1 ? originOrUrl : originOrUrl.slice(protocolIndex + 2);\n var pathIndex = originOrUrl.indexOf(\"/\");\n originOrUrl = pathIndex === -1 ? originOrUrl : protocolIndex === -1 || pathIndex === 0 ? null : originOrUrl.slice(0, pathIndex);\n if (originOrUrl && originOrUrl.slice(-4).toLowerCase() === \".swf\") {\n return null;\n }\n return originOrUrl || null;\n };\n /**\n * Set `allowScriptAccess` based on `trustedDomains` and `window.location.host` vs. `swfPath`.\n *\n * @returns The appropriate script access level.\n * @private\n */\n var _determineScriptAccess = function() {\n var _extractAllDomains = function(origins) {\n var i, len, tmp, resultsArray = [];\n if (typeof origins === \"string\") {\n origins = [ origins ];\n }\n if (!(typeof origins === \"object\" && origins && typeof origins.length === \"number\")) {\n return resultsArray;\n }\n for (i = 0, len = origins.length; i < len; i++) {\n if (_hasOwn.call(origins, i) && (tmp = _extractDomain(origins[i]))) {\n if (tmp === \"*\") {\n resultsArray.length = 0;\n resultsArray.push(\"*\");\n break;\n }\n if (resultsArray.indexOf(tmp) === -1) {\n resultsArray.push(tmp);\n }\n }\n }\n return resultsArray;\n };\n return function(currentDomain, configOptions) {\n var swfDomain = _extractDomain(configOptions.swfPath);\n if (swfDomain === null) {\n swfDomain = currentDomain;\n }\n var trustedDomains = _extractAllDomains(configOptions.trustedDomains);\n var len = trustedDomains.length;\n if (len > 0) {\n if (len === 1 && trustedDomains[0] === \"*\") {\n return \"always\";\n }\n if (trustedDomains.indexOf(currentDomain) !== -1) {\n if (len === 1 && currentDomain === swfDomain) {\n return \"sameDomain\";\n }\n return \"always\";\n }\n }\n return \"never\";\n };\n }();\n /**\n * Get the currently active/focused DOM element.\n *\n * @returns the currently active/focused element, or `null`\n * @private\n */\n var _safeActiveElement = function() {\n try {\n return _document.activeElement;\n } catch (err) {\n return null;\n }\n };\n /**\n * Add a class to an element, if it doesn't already have it.\n *\n * @returns The element, with its new class added.\n * @private\n */\n var _addClass = function(element, value) {\n var c, cl, className, classNames = [];\n if (typeof value === \"string\" && value) {\n classNames = value.split(/\\s+/);\n }\n if (element && element.nodeType === 1 && classNames.length > 0) {\n if (element.classList) {\n for (c = 0, cl = classNames.length; c < cl; c++) {\n element.classList.add(classNames[c]);\n }\n } else if (element.hasOwnProperty(\"className\")) {\n className = \" \" + element.className + \" \";\n for (c = 0, cl = classNames.length; c < cl; c++) {\n if (className.indexOf(\" \" + classNames[c] + \" \") === -1) {\n className += classNames[c] + \" \";\n }\n }\n element.className = className.replace(/^\\s+|\\s+$/g, \"\");\n }\n }\n return element;\n };\n /**\n * Remove a class from an element, if it has it.\n *\n * @returns The element, with its class removed.\n * @private\n */\n var _removeClass = function(element, value) {\n var c, cl, className, classNames = [];\n if (typeof value === \"string\" && value) {\n classNames = value.split(/\\s+/);\n }\n if (element && element.nodeType === 1 && classNames.length > 0) {\n if (element.classList && element.classList.length > 0) {\n for (c = 0, cl = classNames.length; c < cl; c++) {\n element.classList.remove(classNames[c]);\n }\n } else if (element.className) {\n className = (\" \" + element.className + \" \").replace(/[\\r\\n\\t]/g, \" \");\n for (c = 0, cl = classNames.length; c < cl; c++) {\n className = className.replace(\" \" + classNames[c] + \" \", \" \");\n }\n element.className = className.replace(/^\\s+|\\s+$/g, \"\");\n }\n }\n return element;\n };\n /**\n * Attempt to interpret the element's CSS styling. If `prop` is `\"cursor\"`,\n * then we assume that it should be a hand (\"pointer\") cursor if the element\n * is an anchor element (\"a\" tag).\n *\n * @returns The computed style property.\n * @private\n */\n var _getStyle = function(el, prop) {\n var value = _getComputedStyle(el, null).getPropertyValue(prop);\n if (prop === \"cursor\") {\n if (!value || value === \"auto\") {\n if (el.nodeName === \"A\") {\n return \"pointer\";\n }\n }\n }\n return value;\n };\n /**\n * Get the absolutely positioned coordinates of a DOM element.\n *\n * @returns Object containing the element's position, width, and height.\n * @private\n */\n var _getElementPosition = function(el) {\n var pos = {\n left: 0,\n top: 0,\n width: 0,\n height: 0\n };\n if (el.getBoundingClientRect) {\n var elRect = el.getBoundingClientRect();\n var pageXOffset = _window.pageXOffset;\n var pageYOffset = _window.pageYOffset;\n var leftBorderWidth = _document.documentElement.clientLeft || 0;\n var topBorderWidth = _document.documentElement.clientTop || 0;\n var leftBodyOffset = 0;\n var topBodyOffset = 0;\n if (_getStyle(_document.body, \"position\") === \"relative\") {\n var bodyRect = _document.body.getBoundingClientRect();\n var htmlRect = _document.documentElement.getBoundingClientRect();\n leftBodyOffset = bodyRect.left - htmlRect.left || 0;\n topBodyOffset = bodyRect.top - htmlRect.top || 0;\n }\n pos.left = elRect.left + pageXOffset - leftBorderWidth - leftBodyOffset;\n pos.top = elRect.top + pageYOffset - topBorderWidth - topBodyOffset;\n pos.width = \"width\" in elRect ? elRect.width : elRect.right - elRect.left;\n pos.height = \"height\" in elRect ? elRect.height : elRect.bottom - elRect.top;\n }\n return pos;\n };\n /**\n * Determine is an element is visible somewhere within the document (page).\n *\n * @returns Boolean\n * @private\n */\n var _isElementVisible = function(el) {\n if (!el) {\n return false;\n }\n var styles = _getComputedStyle(el, null);\n var hasCssHeight = _parseFloat(styles.height) > 0;\n var hasCssWidth = _parseFloat(styles.width) > 0;\n var hasCssTop = _parseFloat(styles.top) >= 0;\n var hasCssLeft = _parseFloat(styles.left) >= 0;\n var cssKnows = hasCssHeight && hasCssWidth && hasCssTop && hasCssLeft;\n var rect = cssKnows ? null : _getElementPosition(el);\n var isVisible = styles.display !== \"none\" && styles.visibility !== \"collapse\" && (cssKnows || !!rect && (hasCssHeight || rect.height > 0) && (hasCssWidth || rect.width > 0) && (hasCssTop || rect.top >= 0) && (hasCssLeft || rect.left >= 0));\n return isVisible;\n };\n /**\n * Clear all existing timeouts and interval polling delegates.\n *\n * @returns `undefined`\n * @private\n */\n var _clearTimeoutsAndPolling = function() {\n _clearTimeout(_flashCheckTimeout);\n _flashCheckTimeout = 0;\n _clearInterval(_swfFallbackCheckInterval);\n _swfFallbackCheckInterval = 0;\n };\n /**\n * Reposition the Flash object to cover the currently activated element.\n *\n * @returns `undefined`\n * @private\n */\n var _reposition = function() {\n var htmlBridge;\n if (_currentElement && (htmlBridge = _getHtmlBridge(_flashState.bridge))) {\n var pos = _getElementPosition(_currentElement);\n _extend(htmlBridge.style, {\n width: pos.width + \"px\",\n height: pos.height + \"px\",\n top: pos.top + \"px\",\n left: pos.left + \"px\",\n zIndex: \"\" + _getSafeZIndex(_globalConfig.zIndex)\n });\n }\n };\n /**\n * Sends a signal to the Flash object to display the hand cursor if `true`.\n *\n * @returns `undefined`\n * @private\n */\n var _setHandCursor = function(enabled) {\n if (_flashState.ready === true) {\n if (_flashState.bridge && typeof _flashState.bridge.setHandCursor === \"function\") {\n _flashState.bridge.setHandCursor(enabled);\n } else {\n _flashState.ready = false;\n }\n }\n };\n /**\n * Get a safe value for `zIndex`\n *\n * @returns an integer, or \"auto\"\n * @private\n */\n var _getSafeZIndex = function(val) {\n if (/^(?:auto|inherit)$/.test(val)) {\n return val;\n }\n var zIndex;\n if (typeof val === \"number\" && !_isNaN(val)) {\n zIndex = val;\n } else if (typeof val === \"string\") {\n zIndex = _getSafeZIndex(_parseInt(val, 10));\n }\n return typeof zIndex === \"number\" ? zIndex : \"auto\";\n };\n /**\n * Attempt to detect if ZeroClipboard is executing inside of a sandboxed iframe.\n * If it is, Flash Player cannot be used, so ZeroClipboard is dead in the water.\n *\n * @see {@link http://lists.w3.org/Archives/Public/public-whatwg-archive/2014Dec/0002.html}\n * @see {@link https://github.com/zeroclipboard/zeroclipboard/issues/511}\n * @see {@link http://zeroclipboard.org/test-iframes.html}\n *\n * @returns `true` (is sandboxed), `false` (is not sandboxed), or `null` (uncertain) \n * @private\n */\n var _detectSandbox = function(doNotReassessFlashSupport) {\n var effectiveScriptOrigin, frame, frameError, previousState = _flashState.sandboxed, isSandboxed = null;\n doNotReassessFlashSupport = doNotReassessFlashSupport === true;\n if (_pageIsFramed === false) {\n isSandboxed = false;\n } else {\n try {\n frame = window.frameElement || null;\n } catch (e) {\n frameError = {\n name: e.name,\n message: e.message\n };\n }\n if (frame && frame.nodeType === 1 && frame.nodeName === \"IFRAME\") {\n try {\n isSandboxed = frame.hasAttribute(\"sandbox\");\n } catch (e) {\n isSandboxed = null;\n }\n } else {\n try {\n effectiveScriptOrigin = document.domain || null;\n } catch (e) {\n effectiveScriptOrigin = null;\n }\n if (effectiveScriptOrigin === null || frameError && frameError.name === \"SecurityError\" && /(^|[\\s\\(\\[@])sandbox(es|ed|ing|[\\s\\.,!\\)\\]@]|$)/.test(frameError.message.toLowerCase())) {\n isSandboxed = true;\n }\n }\n }\n _flashState.sandboxed = isSandboxed;\n if (previousState !== isSandboxed && !doNotReassessFlashSupport) {\n _detectFlashSupport(_ActiveXObject);\n }\n return isSandboxed;\n };\n /**\n * Detect the Flash Player status, version, and plugin type.\n *\n * @see {@link https://code.google.com/p/doctype-mirror/wiki/ArticleDetectFlash#The_code}\n * @see {@link http://stackoverflow.com/questions/12866060/detecting-pepper-ppapi-flash-with-javascript}\n *\n * @returns `undefined`\n * @private\n */\n var _detectFlashSupport = function(ActiveXObject) {\n var plugin, ax, mimeType, hasFlash = false, isActiveX = false, isPPAPI = false, flashVersion = \"\";\n /**\n * Derived from Apple's suggested sniffer.\n * @param {String} desc e.g. \"Shockwave Flash 7.0 r61\"\n * @returns {String} \"7.0.61\"\n * @private\n */\n function parseFlashVersion(desc) {\n var matches = desc.match(/[\\d]+/g);\n matches.length = 3;\n return matches.join(\".\");\n }\n function isPepperFlash(flashPlayerFileName) {\n return !!flashPlayerFileName && (flashPlayerFileName = flashPlayerFileName.toLowerCase()) && (/^(pepflashplayer\\.dll|libpepflashplayer\\.so|pepperflashplayer\\.plugin)$/.test(flashPlayerFileName) || flashPlayerFileName.slice(-13) === \"chrome.plugin\");\n }\n function inspectPlugin(plugin) {\n if (plugin) {\n hasFlash = true;\n if (plugin.version) {\n flashVersion = parseFlashVersion(plugin.version);\n }\n if (!flashVersion && plugin.description) {\n flashVersion = parseFlashVersion(plugin.description);\n }\n if (plugin.filename) {\n isPPAPI = isPepperFlash(plugin.filename);\n }\n }\n }\n if (_navigator.plugins && _navigator.plugins.length) {\n plugin = _navigator.plugins[\"Shockwave Flash\"];\n inspectPlugin(plugin);\n if (_navigator.plugins[\"Shockwave Flash 2.0\"]) {\n hasFlash = true;\n flashVersion = \"2.0.0.11\";\n }\n } else if (_navigator.mimeTypes && _navigator.mimeTypes.length) {\n mimeType = _navigator.mimeTypes[\"application/x-shockwave-flash\"];\n plugin = mimeType && mimeType.enabledPlugin;\n inspectPlugin(plugin);\n } else if (typeof ActiveXObject !== \"undefined\") {\n isActiveX = true;\n try {\n ax = new ActiveXObject(\"ShockwaveFlash.ShockwaveFlash.7\");\n hasFlash = true;\n flashVersion = parseFlashVersion(ax.GetVariable(\"$version\"));\n } catch (e1) {\n try {\n ax = new ActiveXObject(\"ShockwaveFlash.ShockwaveFlash.6\");\n hasFlash = true;\n flashVersion = \"6.0.21\";\n } catch (e2) {\n try {\n ax = new ActiveXObject(\"ShockwaveFlash.ShockwaveFlash\");\n hasFlash = true;\n flashVersion = parseFlashVersion(ax.GetVariable(\"$version\"));\n } catch (e3) {\n isActiveX = false;\n }\n }\n }\n }\n _flashState.disabled = hasFlash !== true;\n _flashState.outdated = flashVersion && _parseFloat(flashVersion) < _parseFloat(_minimumFlashVersion);\n _flashState.version = flashVersion || \"0.0.0\";\n _flashState.pluginType = isPPAPI ? \"pepper\" : isActiveX ? \"activex\" : hasFlash ? \"netscape\" : \"unknown\";\n };\n /**\n * Invoke the Flash detection algorithms immediately upon inclusion so we're not waiting later.\n */\n _detectFlashSupport(_ActiveXObject);\n /**\n * Always assess the `sandboxed` state of the page at important Flash-related moments.\n */\n _detectSandbox(true);\n /**\n * A shell constructor for `ZeroClipboard` client instances.\n *\n * @constructor\n */\n var ZeroClipboard = function() {\n if (!(this instanceof ZeroClipboard)) {\n return new ZeroClipboard();\n }\n if (typeof ZeroClipboard._createClient === \"function\") {\n ZeroClipboard._createClient.apply(this, _args(arguments));\n }\n };\n /**\n * The ZeroClipboard library's version number.\n *\n * @static\n * @readonly\n * @property {string}\n */\n _defineProperty(ZeroClipboard, \"version\", {\n value: \"2.2.0\",\n writable: false,\n configurable: true,\n enumerable: true\n });\n /**\n * Update or get a copy of the ZeroClipboard global configuration.\n * Returns a copy of the current/updated configuration.\n *\n * @returns Object\n * @static\n */\n ZeroClipboard.config = function() {\n return _config.apply(this, _args(arguments));\n };\n /**\n * Diagnostic method that describes the state of the browser, Flash Player, and ZeroClipboard.\n *\n * @returns Object\n * @static\n */\n ZeroClipboard.state = function() {\n return _state.apply(this, _args(arguments));\n };\n /**\n * Check if Flash is unusable for any reason: disabled, outdated, deactivated, etc.\n *\n * @returns Boolean\n * @static\n */\n ZeroClipboard.isFlashUnusable = function() {\n return _isFlashUnusable.apply(this, _args(arguments));\n };\n /**\n * Register an event listener.\n *\n * @returns `ZeroClipboard`\n * @static\n */\n ZeroClipboard.on = function() {\n return _on.apply(this, _args(arguments));\n };\n /**\n * Unregister an event listener.\n * If no `listener` function/object is provided, it will unregister all listeners for the provided `eventType`.\n * If no `eventType` is provided, it will unregister all listeners for every event type.\n *\n * @returns `ZeroClipboard`\n * @static\n */\n ZeroClipboard.off = function() {\n return _off.apply(this, _args(arguments));\n };\n /**\n * Retrieve event listeners for an `eventType`.\n * If no `eventType` is provided, it will retrieve all listeners for every event type.\n *\n * @returns array of listeners for the `eventType`; if no `eventType`, then a map/hash object of listeners for all event types; or `null`\n */\n ZeroClipboard.handlers = function() {\n return _listeners.apply(this, _args(arguments));\n };\n /**\n * Event emission receiver from the Flash object, forwarding to any registered JavaScript event listeners.\n *\n * @returns For the \"copy\" event, returns the Flash-friendly \"clipData\" object; otherwise `undefined`.\n * @static\n */\n ZeroClipboard.emit = function() {\n return _emit.apply(this, _args(arguments));\n };\n /**\n * Create and embed the Flash object.\n *\n * @returns The Flash object\n * @static\n */\n ZeroClipboard.create = function() {\n return _create.apply(this, _args(arguments));\n };\n /**\n * Self-destruct and clean up everything, including the embedded Flash object.\n *\n * @returns `undefined`\n * @static\n */\n ZeroClipboard.destroy = function() {\n return _destroy.apply(this, _args(arguments));\n };\n /**\n * Set the pending data for clipboard injection.\n *\n * @returns `undefined`\n * @static\n */\n ZeroClipboard.setData = function() {\n return _setData.apply(this, _args(arguments));\n };\n /**\n * Clear the pending data for clipboard injection.\n * If no `format` is provided, all pending data formats will be cleared.\n *\n * @returns `undefined`\n * @static\n */\n ZeroClipboard.clearData = function() {\n return _clearData.apply(this, _args(arguments));\n };\n /**\n * Get a copy of the pending data for clipboard injection.\n * If no `format` is provided, a copy of ALL pending data formats will be returned.\n *\n * @returns `String` or `Object`\n * @static\n */\n ZeroClipboard.getData = function() {\n return _getData.apply(this, _args(arguments));\n };\n /**\n * Sets the current HTML object that the Flash object should overlay. This will put the global\n * Flash object on top of the current element; depending on the setup, this may also set the\n * pending clipboard text data as well as the Flash object's wrapping element's title attribute\n * based on the underlying HTML element and ZeroClipboard configuration.\n *\n * @returns `undefined`\n * @static\n */\n ZeroClipboard.focus = ZeroClipboard.activate = function() {\n return _focus.apply(this, _args(arguments));\n };\n /**\n * Un-overlays the Flash object. This will put the global Flash object off-screen; depending on\n * the setup, this may also unset the Flash object's wrapping element's title attribute based on\n * the underlying HTML element and ZeroClipboard configuration.\n *\n * @returns `undefined`\n * @static\n */\n ZeroClipboard.blur = ZeroClipboard.deactivate = function() {\n return _blur.apply(this, _args(arguments));\n };\n /**\n * Returns the currently focused/\"activated\" HTML element that the Flash object is wrapping.\n *\n * @returns `HTMLElement` or `null`\n * @static\n */\n ZeroClipboard.activeElement = function() {\n return _activeElement.apply(this, _args(arguments));\n };\n /**\n * Keep track of the ZeroClipboard client instance counter.\n */\n var _clientIdCounter = 0;\n /**\n * Keep track of the state of the client instances.\n *\n * Entry structure:\n * _clientMeta[client.id] = {\n * instance: client,\n * elements: [],\n * handlers: {}\n * };\n */\n var _clientMeta = {};\n /**\n * Keep track of the ZeroClipboard clipped elements counter.\n */\n var _elementIdCounter = 0;\n /**\n * Keep track of the state of the clipped element relationships to clients.\n *\n * Entry structure:\n * _elementMeta[element.zcClippingId] = [client1.id, client2.id];\n */\n var _elementMeta = {};\n /**\n * Keep track of the state of the mouse event handlers for clipped elements.\n *\n * Entry structure:\n * _mouseHandlers[element.zcClippingId] = {\n * mouseover: function(event) {},\n * mouseout: function(event) {},\n * mouseenter: function(event) {},\n * mouseleave: function(event) {},\n * mousemove: function(event) {}\n * };\n */\n var _mouseHandlers = {};\n /**\n * Extending the ZeroClipboard configuration defaults for the Client module.\n */\n _extend(_globalConfig, {\n autoActivate: true\n });\n /**\n * The real constructor for `ZeroClipboard` client instances.\n * @private\n */\n var _clientConstructor = function(elements) {\n var client = this;\n client.id = \"\" + _clientIdCounter++;\n _clientMeta[client.id] = {\n instance: client,\n elements: [],\n handlers: {}\n };\n if (elements) {\n client.clip(elements);\n }\n ZeroClipboard.on(\"*\", function(event) {\n return client.emit(event);\n });\n ZeroClipboard.on(\"destroy\", function() {\n client.destroy();\n });\n ZeroClipboard.create();\n };\n /**\n * The underlying implementation of `ZeroClipboard.Client.prototype.on`.\n * @private\n */\n var _clientOn = function(eventType, listener) {\n var i, len, events, added = {}, meta = _clientMeta[this.id], handlers = meta && meta.handlers;\n if (!meta) {\n throw new Error(\"Attempted to add new listener(s) to a destroyed ZeroClipboard client instance\");\n }\n if (typeof eventType === \"string\" && eventType) {\n events = eventType.toLowerCase().split(/\\s+/);\n } else if (typeof eventType === \"object\" && eventType && typeof listener === \"undefined\") {\n for (i in eventType) {\n if (_hasOwn.call(eventType, i) && typeof i === \"string\" && i && typeof eventType[i] === \"function\") {\n this.on(i, eventType[i]);\n }\n }\n }\n if (events && events.length) {\n for (i = 0, len = events.length; i < len; i++) {\n eventType = events[i].replace(/^on/, \"\");\n added[eventType] = true;\n if (!handlers[eventType]) {\n handlers[eventType] = [];\n }\n handlers[eventType].push(listener);\n }\n if (added.ready && _flashState.ready) {\n this.emit({\n type: \"ready\",\n client: this\n });\n }\n if (added.error) {\n for (i = 0, len = _flashStateErrorNames.length; i < len; i++) {\n if (_flashState[_flashStateErrorNames[i].replace(/^flash-/, \"\")]) {\n this.emit({\n type: \"error\",\n name: _flashStateErrorNames[i],\n client: this\n });\n break;\n }\n }\n if (_zcSwfVersion !== undefined && ZeroClipboard.version !== _zcSwfVersion) {\n this.emit({\n type: \"error\",\n name: \"version-mismatch\",\n jsVersion: ZeroClipboard.version,\n swfVersion: _zcSwfVersion\n });\n }\n }\n }\n return this;\n };\n /**\n * The underlying implementation of `ZeroClipboard.Client.prototype.off`.\n * @private\n */\n var _clientOff = function(eventType, listener) {\n var i, len, foundIndex, events, perEventHandlers, meta = _clientMeta[this.id], handlers = meta && meta.handlers;\n if (!handlers) {\n return this;\n }\n if (arguments.length === 0) {\n events = _keys(handlers);\n } else if (typeof eventType === \"string\" && eventType) {\n events = eventType.split(/\\s+/);\n } else if (typeof eventType === \"object\" && eventType && typeof listener === \"undefined\") {\n for (i in eventType) {\n if (_hasOwn.call(eventType, i) && typeof i === \"string\" && i && typeof eventType[i] === \"function\") {\n this.off(i, eventType[i]);\n }\n }\n }\n if (events && events.length) {\n for (i = 0, len = events.length; i < len; i++) {\n eventType = events[i].toLowerCase().replace(/^on/, \"\");\n perEventHandlers = handlers[eventType];\n if (perEventHandlers && perEventHandlers.length) {\n if (listener) {\n foundIndex = perEventHandlers.indexOf(listener);\n while (foundIndex !== -1) {\n perEventHandlers.splice(foundIndex, 1);\n foundIndex = perEventHandlers.indexOf(listener, foundIndex);\n }\n } else {\n perEventHandlers.length = 0;\n }\n }\n }\n }\n return this;\n };\n /**\n * The underlying implementation of `ZeroClipboard.Client.prototype.handlers`.\n * @private\n */\n var _clientListeners = function(eventType) {\n var copy = null, handlers = _clientMeta[this.id] && _clientMeta[this.id].handlers;\n if (handlers) {\n if (typeof eventType === \"string\" && eventType) {\n copy = handlers[eventType] ? handlers[eventType].slice(0) : [];\n } else {\n copy = _deepCopy(handlers);\n }\n }\n return copy;\n };\n /**\n * The underlying implementation of `ZeroClipboard.Client.prototype.emit`.\n * @private\n */\n var _clientEmit = function(event) {\n if (_clientShouldEmit.call(this, event)) {\n if (typeof event === \"object\" && event && typeof event.type === \"string\" && event.type) {\n event = _extend({}, event);\n }\n var eventCopy = _extend({}, _createEvent(event), {\n client: this\n });\n _clientDispatchCallbacks.call(this, eventCopy);\n }\n return this;\n };\n /**\n * The underlying implementation of `ZeroClipboard.Client.prototype.clip`.\n * @private\n */\n var _clientClip = function(elements) {\n if (!_clientMeta[this.id]) {\n throw new Error(\"Attempted to clip element(s) to a destroyed ZeroClipboard client instance\");\n }\n elements = _prepClip(elements);\n for (var i = 0; i < elements.length; i++) {\n if (_hasOwn.call(elements, i) && elements[i] && elements[i].nodeType === 1) {\n if (!elements[i].zcClippingId) {\n elements[i].zcClippingId = \"zcClippingId_\" + _elementIdCounter++;\n _elementMeta[elements[i].zcClippingId] = [ this.id ];\n if (_globalConfig.autoActivate === true) {\n _addMouseHandlers(elements[i]);\n }\n } else if (_elementMeta[elements[i].zcClippingId].indexOf(this.id) === -1) {\n _elementMeta[elements[i].zcClippingId].push(this.id);\n }\n var clippedElements = _clientMeta[this.id] && _clientMeta[this.id].elements;\n if (clippedElements.indexOf(elements[i]) === -1) {\n clippedElements.push(elements[i]);\n }\n }\n }\n return this;\n };\n /**\n * The underlying implementation of `ZeroClipboard.Client.prototype.unclip`.\n * @private\n */\n var _clientUnclip = function(elements) {\n var meta = _clientMeta[this.id];\n if (!meta) {\n return this;\n }\n var clippedElements = meta.elements;\n var arrayIndex;\n if (typeof elements === \"undefined\") {\n elements = clippedElements.slice(0);\n } else {\n elements = _prepClip(elements);\n }\n for (var i = elements.length; i--; ) {\n if (_hasOwn.call(elements, i) && elements[i] && elements[i].nodeType === 1) {\n arrayIndex = 0;\n while ((arrayIndex = clippedElements.indexOf(elements[i], arrayIndex)) !== -1) {\n clippedElements.splice(arrayIndex, 1);\n }\n var clientIds = _elementMeta[elements[i].zcClippingId];\n if (clientIds) {\n arrayIndex = 0;\n while ((arrayIndex = clientIds.indexOf(this.id, arrayIndex)) !== -1) {\n clientIds.splice(arrayIndex, 1);\n }\n if (clientIds.length === 0) {\n if (_globalConfig.autoActivate === true) {\n _removeMouseHandlers(elements[i]);\n }\n delete elements[i].zcClippingId;\n }\n }\n }\n }\n return this;\n };\n /**\n * The underlying implementation of `ZeroClipboard.Client.prototype.elements`.\n * @private\n */\n var _clientElements = function() {\n var meta = _clientMeta[this.id];\n return meta && meta.elements ? meta.elements.slice(0) : [];\n };\n /**\n * The underlying implementation of `ZeroClipboard.Client.prototype.destroy`.\n * @private\n */\n var _clientDestroy = function() {\n if (!_clientMeta[this.id]) {\n return;\n }\n this.unclip();\n this.off();\n delete _clientMeta[this.id];\n };\n /**\n * Inspect an Event to see if the Client (`this`) should honor it for emission.\n * @private\n */\n var _clientShouldEmit = function(event) {\n if (!(event && event.type)) {\n return false;\n }\n if (event.client && event.client !== this) {\n return false;\n }\n var meta = _clientMeta[this.id];\n var clippedEls = meta && meta.elements;\n var hasClippedEls = !!clippedEls && clippedEls.length > 0;\n var goodTarget = !event.target || hasClippedEls && clippedEls.indexOf(event.target) !== -1;\n var goodRelTarget = event.relatedTarget && hasClippedEls && clippedEls.indexOf(event.relatedTarget) !== -1;\n var goodClient = event.client && event.client === this;\n if (!meta || !(goodTarget || goodRelTarget || goodClient)) {\n return false;\n }\n return true;\n };\n /**\n * Handle the actual dispatching of events to a client instance.\n *\n * @returns `undefined`\n * @private\n */\n var _clientDispatchCallbacks = function(event) {\n var meta = _clientMeta[this.id];\n if (!(typeof event === \"object\" && event && event.type && meta)) {\n return;\n }\n var async = _shouldPerformAsync(event);\n var wildcardTypeHandlers = meta && meta.handlers[\"*\"] || [];\n var specificTypeHandlers = meta && meta.handlers[event.type] || [];\n var handlers = wildcardTypeHandlers.concat(specificTypeHandlers);\n if (handlers && handlers.length) {\n var i, len, func, context, eventCopy, originalContext = this;\n for (i = 0, len = handlers.length; i < len; i++) {\n func = handlers[i];\n context = originalContext;\n if (typeof func === \"string\" && typeof _window[func] === \"function\") {\n func = _window[func];\n }\n if (typeof func === \"object\" && func && typeof func.handleEvent === \"function\") {\n context = func;\n func = func.handleEvent;\n }\n if (typeof func === \"function\") {\n eventCopy = _extend({}, event);\n _dispatchCallback(func, context, [ eventCopy ], async);\n }\n }\n }\n };\n /**\n * Prepares the elements for clipping/unclipping.\n *\n * @returns An Array of elements.\n * @private\n */\n var _prepClip = function(elements) {\n if (typeof elements === \"string\") {\n elements = [];\n }\n return typeof elements.length !== \"number\" ? [ elements ] : elements;\n };\n /**\n * Add a `mouseover` handler function for a clipped element.\n *\n * @returns `undefined`\n * @private\n */\n var _addMouseHandlers = function(element) {\n if (!(element && element.nodeType === 1)) {\n return;\n }\n var _suppressMouseEvents = function(event) {\n if (!(event || (event = _window.event))) {\n return;\n }\n if (event._source !== \"js\") {\n event.stopImmediatePropagation();\n event.preventDefault();\n }\n delete event._source;\n };\n var _elementMouseOver = function(event) {\n if (!(event || (event = _window.event))) {\n return;\n }\n _suppressMouseEvents(event);\n ZeroClipboard.focus(element);\n };\n element.addEventListener(\"mouseover\", _elementMouseOver, false);\n element.addEventListener(\"mouseout\", _suppressMouseEvents, false);\n element.addEventListener(\"mouseenter\", _suppressMouseEvents, false);\n element.addEventListener(\"mouseleave\", _suppressMouseEvents, false);\n element.addEventListener(\"mousemove\", _suppressMouseEvents, false);\n _mouseHandlers[element.zcClippingId] = {\n mouseover: _elementMouseOver,\n mouseout: _suppressMouseEvents,\n mouseenter: _suppressMouseEvents,\n mouseleave: _suppressMouseEvents,\n mousemove: _suppressMouseEvents\n };\n };\n /**\n * Remove a `mouseover` handler function for a clipped element.\n *\n * @returns `undefined`\n * @private\n */\n var _removeMouseHandlers = function(element) {\n if (!(element && element.nodeType === 1)) {\n return;\n }\n var mouseHandlers = _mouseHandlers[element.zcClippingId];\n if (!(typeof mouseHandlers === \"object\" && mouseHandlers)) {\n return;\n }\n var key, val, mouseEvents = [ \"move\", \"leave\", \"enter\", \"out\", \"over\" ];\n for (var i = 0, len = mouseEvents.length; i < len; i++) {\n key = \"mouse\" + mouseEvents[i];\n val = mouseHandlers[key];\n if (typeof val === \"function\") {\n element.removeEventListener(key, val, false);\n }\n }\n delete _mouseHandlers[element.zcClippingId];\n };\n /**\n * Creates a new ZeroClipboard client instance.\n * Optionally, auto-`clip` an element or collection of elements.\n *\n * @constructor\n */\n ZeroClipboard._createClient = function() {\n _clientConstructor.apply(this, _args(arguments));\n };\n /**\n * Register an event listener to the client.\n *\n * @returns `this`\n */\n ZeroClipboard.prototype.on = function() {\n return _clientOn.apply(this, _args(arguments));\n };\n /**\n * Unregister an event handler from the client.\n * If no `listener` function/object is provided, it will unregister all handlers for the provided `eventType`.\n * If no `eventType` is provided, it will unregister all handlers for every event type.\n *\n * @returns `this`\n */\n ZeroClipboard.prototype.off = function() {\n return _clientOff.apply(this, _args(arguments));\n };\n /**\n * Retrieve event listeners for an `eventType` from the client.\n * If no `eventType` is provided, it will retrieve all listeners for every event type.\n *\n * @returns array of listeners for the `eventType`; if no `eventType`, then a map/hash object of listeners for all event types; or `null`\n */\n ZeroClipboard.prototype.handlers = function() {\n return _clientListeners.apply(this, _args(arguments));\n };\n /**\n * Event emission receiver from the Flash object for this client's registered JavaScript event listeners.\n *\n * @returns For the \"copy\" event, returns the Flash-friendly \"clipData\" object; otherwise `undefined`.\n */\n ZeroClipboard.prototype.emit = function() {\n return _clientEmit.apply(this, _args(arguments));\n };\n /**\n * Register clipboard actions for new element(s) to the client.\n *\n * @returns `this`\n */\n ZeroClipboard.prototype.clip = function() {\n return _clientClip.apply(this, _args(arguments));\n };\n /**\n * Unregister the clipboard actions of previously registered element(s) on the page.\n * If no elements are provided, ALL registered elements will be unregistered.\n *\n * @returns `this`\n */\n ZeroClipboard.prototype.unclip = function() {\n return _clientUnclip.apply(this, _args(arguments));\n };\n /**\n * Get all of the elements to which this client is clipped.\n *\n * @returns array of clipped elements\n */\n ZeroClipboard.prototype.elements = function() {\n return _clientElements.apply(this, _args(arguments));\n };\n /**\n * Self-destruct and clean up everything for a single client.\n * This will NOT destroy the embedded Flash object.\n *\n * @returns `undefined`\n */\n ZeroClipboard.prototype.destroy = function() {\n return _clientDestroy.apply(this, _args(arguments));\n };\n /**\n * Stores the pending plain text to inject into the clipboard.\n *\n * @returns `this`\n */\n ZeroClipboard.prototype.setText = function(text) {\n if (!_clientMeta[this.id]) {\n throw new Error(\"Attempted to set pending clipboard data from a destroyed ZeroClipboard client instance\");\n }\n ZeroClipboard.setData(\"text/plain\", text);\n return this;\n };\n /**\n * Stores the pending HTML text to inject into the clipboard.\n *\n * @returns `this`\n */\n ZeroClipboard.prototype.setHtml = function(html) {\n if (!_clientMeta[this.id]) {\n throw new Error(\"Attempted to set pending clipboard data from a destroyed ZeroClipboard client instance\");\n }\n ZeroClipboard.setData(\"text/html\", html);\n return this;\n };\n /**\n * Stores the pending rich text (RTF) to inject into the clipboard.\n *\n * @returns `this`\n */\n ZeroClipboard.prototype.setRichText = function(richText) {\n if (!_clientMeta[this.id]) {\n throw new Error(\"Attempted to set pending clipboard data from a destroyed ZeroClipboard client instance\");\n }\n ZeroClipboard.setData(\"application/rtf\", richText);\n return this;\n };\n /**\n * Stores the pending data to inject into the clipboard.\n *\n * @returns `this`\n */\n ZeroClipboard.prototype.setData = function() {\n if (!_clientMeta[this.id]) {\n throw new Error(\"Attempted to set pending clipboard data from a destroyed ZeroClipboard client instance\");\n }\n ZeroClipboard.setData.apply(this, _args(arguments));\n return this;\n };\n /**\n * Clears the pending data to inject into the clipboard.\n * If no `format` is provided, all pending data formats will be cleared.\n *\n * @returns `this`\n */\n ZeroClipboard.prototype.clearData = function() {\n if (!_clientMeta[this.id]) {\n throw new Error(\"Attempted to clear pending clipboard data from a destroyed ZeroClipboard client instance\");\n }\n ZeroClipboard.clearData.apply(this, _args(arguments));\n return this;\n };\n /**\n * Gets a copy of the pending data to inject into the clipboard.\n * If no `format` is provided, a copy of ALL pending data formats will be returned.\n *\n * @returns `String` or `Object`\n */\n ZeroClipboard.prototype.getData = function() {\n if (!_clientMeta[this.id]) {\n throw new Error(\"Attempted to get pending clipboard data from a destroyed ZeroClipboard client instance\");\n }\n return ZeroClipboard.getData.apply(this, _args(arguments));\n };\n if (typeof define === \"function\" && define.amd) {\n define(function() {\n return ZeroClipboard;\n });\n } else if (typeof module === \"object\" && module && typeof module.exports === \"object\" && module.exports) {\n module.exports = ZeroClipboard;\n } else {\n window.ZeroClipboard = ZeroClipboard;\n }\n})(function() {\n return this || window;\n}());"]} \ No newline at end of file diff --git a/bower_components/zeroclipboard/dist/ZeroClipboard.swf b/bower_components/zeroclipboard/dist/ZeroClipboard.swf new file mode 100644 index 0000000000000000000000000000000000000000..8bad6a3e34f1b0b055da3ee8e506fe627cf75987 GIT binary patch literal 6580 zcmV;l8B69vS5pqYF8}~|+O;|hc$3$4_xw*umW3pYG4CHRmTj(WK^{#o5R7es9YDkn zLI7j^mVQ|RS#tDCek9Nm66e+C)g+`1B~@Q(+R&s)wsqasbxqrKYqw64hv~YlTf24b z-dp$X9_-xz|0UVRr0w^$##i^=bI(2J+;h%7=Uka7B7Bb!*UN;|0qOFrB80qW`8R-& zo#{w)M_*S!nHozZ^&L3d<%?x9<2%~gCMPFbCvR#^rAOPg-G2M+ZT{_T+qbtMMvFe3 z%&1c>Nqw_#w?Uvw)5GcbcqX1oDr~Nfq_UY^KA%N35-t-R&!!Uw$w;_OOK4+SGNZR` zYu$!~BH{iFe6Y;Rh$hA$i=&@Az^rSkWwL}xD9^2VgL1cR}@k~P7-4RKR zXi6ZVO({1k9hLnCwu!=!BNah+&pW2dcDII8V{PN!wIJ~HIrJ1QG&7cgqj@9s-xQO?nA~}d2Yz4X0+X1T3FeBJ5{#( zx7}=t!Js?a7HQjppaFN2F82@loureycJ10R2agt(unE#fjuGSMt3Pgdp%*RwO~)}U zo$5@)$463XIzql!bGC<p-c3I;L$SZqiB^5t(qr`gxNh!bB<_k@u!j2~AB(!I4v1 zI8$vHq%{)P#}n$b)IXk%XEdh?)Fzl|9S2icUF&9JsXvpBCr538Y%XBvK5Y7BuASOeHH4yVXdO`u_N6tc-<3JJm#DL=B&I zrM0otgjSjgVH^m$mWVkhr~dd#iut z;lAL}p6;&U&W-~I_I7mMIefUUJJ55qYIqoZdMd6(hKFl<`-i(a20Cyl*xB9RAM6_r z9C}EIS{6|dF|6r&7oM<@9g`}$al*qUJe3Y4o0R(%B_358Gt=W*DylFvW!J7<$|e(a zlhV*&09zAUax@cDb}N2kk7A)-Ptsu3)UGIJ6-7(v+S24xhGyS^Jcb0NaY9Wiab)6e z$8P6*OayG*YN4949FGqn=HfFWbg5&vvt4I@HW`(TyOd-$kytDsvK&|9Ndu=W5hG}b z8&mb*WDBe%7q_emf38v$t@ua@d%7~S0*wKGe+jhVI*0zysJduefYZe6j9cI9< zUkL0ndovPtrm#|oqc9s5BK0IO#;a)c3C%=Y^A1GT&d(@2j5D%zH50~sE3#r?x=Npn zmSS&hW67Z!jV%aA2Xc57gl-4kgnAvICQa+AujPKti2XA_xrha+ZQYDO6Yf)y-q*190&8Ii+RlGyi7sI4qR zEy8CfTWhjrv32p5c87qM-Es2zsP$6{@ly#+}WmD%njk|Grp*?qY6nZ%RI&%Xvrqk2{oQj zM-rOLghaH_v>MS^>2w536f}u@!t7hd;`$i7Bo0)qwYDsrN=DXa0YxFT=qRbdp ze7>gEOzJ>tQcHKLx`s+IZx&N1Mk?Jn*=@=KJ+Yj>B-sgOD3h+~Xf`Y1J0!Df`UzK2 zFW<#grMp;BiRHU+$Hvh_YD7)6+=o}HT#=javX+r_JTj_DX6*q~#0VbG2$*6tM?Xt% z9f|m8(l!u0Jba)#Fd(XI>M~aPvGCOnq8{ztM>{$Ph66nVcD)318Yp+6Gh+4<_ICFV z$oHBP8+!hDEFRYFBO2zfKs=Ff7>V@mcq9|6GKTv#ti3a~NLrojLoY#PKY{!U(4uo~V__V-2>}nKK6dTEUHq0ssft4GQaV2$ug-3ff8PTGc-6E2j$lR$- zJ1}RC;YGor%$^D+D49v85)2g1q*-s%WAUho&fXoJsVqK@B8VPO*jW;1b~#!e-6I+2 zI4TdDy*a(dn%?7DST4eF)jXHt(T<3i%3^SF91ITjcMl&79_hA*IO?-o!@&Uy*cH6{ zkj)$p9u_+f^mN|2Y*EIoUXYwU*QN5>r7Hl*ynvD!S-sbnhu-T8Ia?_VOR*FyRH{ef zkvJNO1rar2!)k#=!pO3y^|+5x&;BN6ts&9y!eXtG8e5NT*f2Er}E}=FdY@wDfk;bo8D`|75iEZqrLm`~d)U>-i4;WvM>s15YN3og; z_8sgPkWm(cqeV@KWyTU!`v(plun_okCR#no9`u6)0Vaa(#!S3_cpeRLgn_ZN(RHjn zWTO=7>0)1Agilvv@a<=V0W1bI$^3%e*{P1JBXLa4aZTq^x@g!{#4MSelKQk!e135! zPQWOt0V{^Ew)W)lj+SF;%Y8#u?{B$%c*lvB6Rktd^{7%kBge7knhHi?^SUjF6Hr^C zh%lV%xvkxw>a{@9H~OA6R`*B&(Oz{x;P958%jC`#4woK29V!ZjhWJNYyT3 zncL%CUL)I9faH>EL6RM-aAaRCuajWCt7^k9fef4sybOZONiuJf!7hVChAJ7HvREyP zE?HV8i*8x+$f8#km&;;}EUu7IK5?Zi*2&^3SzImK*2p%mY+EbCIvLi>ut5ezhK(|8 zl3}y#sFxjH8Meq$gA6{|)+F1u%C;8S)+XDw$+qpX?IziAv+TG z!NWa@?J*obj{SSFy${DvVE-hxr?5SZ?HO$E$M!6?=deAG?EYuNt|w%-N&ml*u(V1FAp`Q}J)Dg*x>p6mCK;U8iDZAAZL?EeJY-(vea zY=4hy|B3Mb!uDfqUjVu8f3f`^*q8CL^Fhx35GTkm!{KQJJdI#~1n1JDoGd-Y$!p%r z$!niLHcw)E3dc`l{|vVGbN1)hzUMjn1=hcSun*v#4`M$m!;9Em#CD03ogcpsib-{3@h1K(Qu0}lK94ghBiIKuuo#G1f> zA|V_v@x%sBr!fOTC~sqb0+P+Y4w)YoG0ylWnF!Y3i3AQ^pKt&Y!;px;MrBeQ@I2wn zPvDTd@xk*pNiqrA03VoO^*m4Ji;f=#8pl3(sj@}{JN_f!bPysromGT5oz7}PTu$dQ zBR%wtAcnQM^?T+3N+O0tXqI-fJ94IWdG9av9`1dtIPbGp1{a$Bn0?Q94RvgAiQ7xxb5ZTv<17C2p>KjSypd z*M(*ex_VOt#|=1k;n-X-AeS$Ax0QCDrl-u6^UWS~)F$OTO&Cb!`5@0oCk)a&600(a zacqi|wwEZ4(IgGh6a!aijWf&&DS?sNh&vE01XnV((tG#t!S!68rY*+CQT7p`8?Ofm z5WFY*2;P%+f_GUIc)Q~n$orL{LOGEG=A z)ntxN<>+*d-j}2I=jfRnJ)5KNHq7&$96bk?aRLg=%oV*W2nlToZJ`f9F&DTM0yn`q z-i>EM8BmJCe!Q_Z!S1-sBtrnUIc4T5s^dJ+eMbn@eDstuC@_fw9`vwgOwgohkg)A@ z#pqt^3^;n8J_wh+%L&2p%+=7A*}yH}#mG~bRreO7d&-D=OhhNH;DkN|f&HNFDMmZ# z!*IFTBN<4Y4VR6*4QgjG+QoLPmW&NuY8Mk)7CE~k$gWhVC)%stiT#1-LG=*!!RTT2 zt|6gUK)<|Fi1xKWuph9mC(WUEMF+4A29F4ZHc;cLog5$HvjjUJ;Pf+D?yqvM9h;qA5aNM~2xUsP&P zU5wIPv7nv^4Y7TOxxF64zWJ!57*IF_yB%dfa3eR1!eAj`cf4U5=BhFi77RKC?{kFE z9I)Sy0R1S?j{*HS&`%h@F9ZD~&{vG#PZ__T2Kp+{%RoN^gLWiTPr~Bt`LN_}Lor9B z5pg|IE{q6)lVF($C(&W$)eBw>;l)`tF(B&jc1iSn{n|B~WD|?@b<}Anppu+=qDbEW zbtGSmo+z5fuj0`m6)g zSS&EhY+huU6nHUS;t1)XB`ku>8gr&Km}-{s9M9VkBQ%c72ymAH_nH7t6$m0KFHgS& z3Vj=V^vl@3g6*r|8x&)8M#fsjGM1O;MaK(J%052KD`^&NRXrcA3#Fr(a$r=mCS;=F z^7&FYtBYnYTsW^zhSCkz2Ko)ac!a5K%3X_Y?l1U{k>JJl*isI+8EO|;?lW@PHy3H`Pl0~NC>+e`pRuuEj{h7`#oq<`J)nO9^e=&a zALw5J{Q=M)0@LYV1N|GO*5#&Jg9>17JP%k!k_WIXl*xlYc$qwe<)K7YRr7*)kC7e? zY460mmSXcGp#K8&UxEG`D%5t3xfb zXvXtbStws4ih}s8k^NZ!77E^n&^qj#%URS{@}-?vvbp07R2RL^6T(0b2EMxiL&9w6 zVJNcuA9w`NbXP}&ddL-{kAf4=GE4s(a#!CLMg#^~k-v6r=rI_29OyMB^EoazL}&4c zIr;#X>wO%6p67y(0Sne^F{bkFynpDuz=A4&6xPL@2w#U_0SizZu1E3hk&@4q+${7y zv!Z+gf=>b)a?z)tL45+;x3Zfv%Wh6su&$1==)*;0_%wuE8kYe25Jw;80^9lEGk{l1 z*az;W7TX!_{lH47XVFV|vVk%G5?}H7=YV-|S&HW&dI5qj0I`Izg0{U>IX(ds-s46M z$V4#FLmzX z$QW*NBExNYKOHSZv2=TpD@3t;yNDf@a3A6dfsX^JJV0LJiqt(jDB;G<0w;*|m@`73 zfauF;sd_Q+N#L;hq95fhUx;mDnY$3&%;H=Ly&-!pnmAvKfSY zRyA21Ba3Nb3bU*}WG+@jW6%23S72WK5pop*e()j>HrfUgVF9`0 ziwjHmrV(L+k-(K=u$42V@|y6{li(?K4KI|j+PESfx}g-8K4rxvZe7fUEPR!?)F?{4 z@Cu%&p0HPoQLn9bvKLPwa5JmX9;;MZHp3{NHr%{{6_wE|6BGRg#14X#L6Ph4w-iV literal 0 HcmV?d00001 diff --git a/index.html b/index.html new file mode 100644 index 0000000..dbe0915 --- /dev/null +++ b/index.html @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + +
+ + + \ No newline at end of file diff --git a/lib/.DS_Store b/lib/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..5008ddfcf53c02e82d7eee2e57c38e5672ef89f6 GIT binary patch literal 6148 zcmeH~Jr2S!425mzP>H1@V-^m;4Wg<&0T*E43hX&L&p$$qDprKhvt+--jT7}7np#A3 zem<@ulZcFPQ@L2!n>{z**++&mCkOWA81W14cNZlEfg7;MkzE(HCqgga^y>{tEnwC%0;vJ&^%eQ zLs35+`xjp>T0>> 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; + } + }; +} + +if (!Array.isArray) { + Array.isArray = function(obj) { + return toString.call(obj) == '[object Array]'; + }; +} + +// From https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/keys +// License CC-BY-SA v2.5 +if (!Object.keys) { + Object.keys = (function() { + 'use strict'; + var hasOwnProperty = Object.prototype.hasOwnProperty, + hasDontEnumBug = !({ toString: null }).propertyIsEnumerable('toString'), + dontEnums = [ + 'toString', + 'toLocaleString', + 'valueOf', + 'hasOwnProperty', + 'isPrototypeOf', + 'propertyIsEnumerable', + 'constructor' + ], + dontEnumsLength = dontEnums.length; + + return function(obj) { + if (typeof obj !== 'object' && (typeof obj !== 'function' || obj === null)) { + throw new TypeError('Object.keys called on non-object'); + } + + var result = [], prop, i; + + for (prop in obj) { + if (hasOwnProperty.call(obj, prop)) { + result.push(prop); + } + } + + if (hasDontEnumBug) { + for (i = 0; i < dontEnumsLength; i++) { + if (hasOwnProperty.call(obj, dontEnums[i])) { + result.push(dontEnums[i]); + } + } + } + return result; + }; + }()); +} + +/* + * 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 DOM 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 () {} + , eventManager = Handsontable.eventManager(instance); + + 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.insertBefore(this.container, rootElement.firstChild); + + this.guid = 'ht_' + Handsontable.helper.randomString(); //this is the namespace for global events + + if (!this.rootElement.id || this.rootElement.id.substring(0, 3) === "ht_") { + this.rootElement.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": + // //column order may have changes, so we need to translate the selection column index -> source array index + // index = instance.runHooksAndReturn('modifyCol', index); + delta = datamap.createCol(index, amount); + + if (delta) { + + if(Array.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": + //column order may have changes, so we need to translate the selection column index -> source array index + index = instance.runHooks('modifyCol', index); + + 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(Array.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; + + /* jshint ignore:start */ + default: + throw new Error('There is no such action "' + action + '"'); + break; + /* jshint ignore:end */ + } + + 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"] + * @param {String} direction (left|right|up|down) + * @param {Array} deltas array + * @return {Object|undefined} ending td in pasted area (only if any cell was changed) + */ + populateFromArray: function (start, input, end, source, method, direction, deltas) { + 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; + + /* jshint ignore:start */ + case 'overwrite': + default: + /* jshint ignore:end */ + // overwrite and other not specified options + current.row = start.row; + current.col = start.col; + + var iterators = {row: 0, col: 0}, // number of packages + selected = { // selected range + row: (end && start) ? (end.row - start.row + 1) : 1, + col: (end && start) ? (end.col - start.col + 1) : 1 + }; + + if (['up', 'left'].indexOf(direction) !== -1) { + iterators = { + row: Math.ceil(selected.row / rlen) || 1, + col: Math.ceil(selected.col / input[0].length) || 1 + }; + } else if (['down', 'right'].indexOf(direction) !== -1) { + iterators = { + row: 1, + col: 1 + }; + } + + + for (r = 0; r < rlen; r++) { + if ((end && current.row > end.row) || (!priv.settings.allowInsertRow && 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.allowInsertColumn && current.col > instance.countCols() - 1) || (current.col >= priv.settings.maxCols)) { + break; + } + + if (!instance.getCellMeta(current.row, current.col).readOnly) { + var result, + value = input[r][c], + index = { + row: r, + col: c + }; + + if (source === 'autofill') { + result = instance.runHooks('beforeAutofillInsidePopulate', index, direction, input, deltas, iterators, selected); + + if (result) { + iterators = typeof(result.iterators) !== 'undefined' ? result.iterators : iterators; + value = typeof(result.value) !== 'undefined' ? result.value : value; + } + } + + setData.push([current.row, current.col, value]); + } + + current.col++; + + if (end && c === clen - 1) { + c = -1; + + if (['down', 'right'].indexOf(direction) !== -1) { + iterators.col++; + } else if (['up', 'left'].indexOf(direction) !== -1) { + if (iterators.col > 1) { + iterators.col--; + } + } + + } + } + + current.row++; + iterators.col = 1; + + if (end && r === rlen - 1) { + r = -1; + + if (['down', 'right'].indexOf(direction) !== -1) { + iterators.row++; + } else if (['up', 'left'].indexOf(direction) !== -1) { + if (iterators.row > 1) { + iterators.row--; + } + } + + } + } + instance.setDataAtCell(setData, null, null, source || 'populateFromArray'); + break; + } + } + }; + + this.selection = selection = { //this public assignment is only temporary + inProgress: false, + + selectedHeader: { + cols: false, + rows: false + }, + + setSelectedHeaders: function (rows, cols) { + instance.selection.selectedHeader.rows = rows; + instance.selection.selectedHeader.cols = cols; + }, + + /** + * 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, keepEditorOpened) { + Handsontable.hooks.run(instance, "beforeSetRangeStart", coords); + priv.selRange = new WalkontableCellRange(coords, coords, coords); + selection.setRangeEnd(coords, null, keepEditorOpened); + }, + + /** + * 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, keepEditorOpened) { + //trigger handlers + Handsontable.hooks.run(instance, "beforeSetRangeEnd", coords); + + instance.selection.begin(); + + priv.selRange.to = new WalkontableCellCoords(coords.row, coords.col); + 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()) { + if(priv.selRange.from) { + instance.view.scrollViewport(priv.selRange.from); + } else { + instance.view.scrollViewport(coords); + } + + } + selection.refreshBorders(null, keepEditorOpened); + }, + + /** + * 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 () { + var isMultiple = !(priv.selRange.to.col === priv.selRange.from.col && priv.selRange.to.row === priv.selRange.from.row) + , modifier = Handsontable.hooks.run(instance, 'afterIsMultipleSelection', isMultiple); + + if(isMultiple) { + return modifier; + } + }, + + /** + * Selects cell relative to current cell (if possible) + */ + transformStart: function (rowDelta, colDelta, force, keepEditorOpened) { + var delta = new WalkontableCellCoords(rowDelta, colDelta); + instance.runHooks('modifyTransformStart', delta); + + /* jshint ignore:start */ + 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; + } + /* jshint ignore:end */ + + 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, keepEditorOpened); + }, + + /** + * 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'); + + if(Handsontable.mobileBrowser) { + Handsontable.Dom.addClass(instance.rootElement, 'mobile'); + } + + 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 () { + /* jshint ignore:start */ + if (this.validatorsInQueue == 0 && resolved == false) { + resolved = true; + this.onQueueEmpty(); + } + /* jshint ignore:end */ + } + }; + } + + 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]); + // column order may have changes, so we need to translate physical col index (stored in datasource) to logical (displayed to user) + var logicalCol = instance.runHooks('modifyCol', col); + 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]) || cellProperties.format )) { + var len = changes[i][3].length; + if (typeof cellProperties.language == 'undefined') { + numeral.language('en'); + } + // this input in format XXXX.XX is likely to come from paste. Let's parse it using international rules + else if (changes[i][3].indexOf(".") === len - 3 && changes[i][3].indexOf(",") === -1) { + numeral.language('en'); + } + else { + numeral.language(cellProperties.language); + } + if (numeral.validate(changes[i][3])) { + changes[i][3] = numeral().unformat(changes[i][3]); + } + } + } + + /* jshint ignore:start */ + 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); + } + /* jshint ignore:end */ + } + } + waitingForValidator.checkIfQueueIsEmpty(); + + function resolve() { + var beforeChangeResult; + + if (changes.length) { + beforeChangeResult = Handsontable.hooks.run(instance, "beforeChange", changes, source); + if (typeof beforeChangeResult === 'function') { + console.warn("Your beforeChange callback returns a function. It's not supported since Handsontable 0.12.1 (and the returned function will not be executed)."); + } else if (beforeChangeResult === false) { + changes.splice(0, changes.length); //invalidate all changes (remove everything from array) + } + } + 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.allowInsertRow) { + while (changes[i][0] > instance.countRows() - 1) { + datamap.createRow(); + } + } + + if (instance.dataType === 'array' && priv.settings.allowInsertColumn) { + 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.run(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) { + valid = Handsontable.hooks.run(instance, "afterValidate", valid, value, cellProperties.row, cellProperties.prop, source); + cellProperties.valid = valid; + + callback(valid); + Handsontable.hooks.run(instance, "postAfterValidate", valid, value, cellProperties.row, cellProperties.prop, source); + }); + }, 0)); + + } else { + //resolve callback even if validator function was not found + cellProperties.valid = true; + callback(true); + } + }; + + function setDataInputToArray(row, propOrCol, value) { + if (typeof row === "object") { //is it an array of changes + return row; + } + else { + return [ + [row, propOrCol, 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"] + * @param {String} direction edit (left|right|up|down) + * @param {Array} deltas array + * @return {Object|undefined} ending td in pasted area (only if any cell was changed) + */ + this.populateFromArray = function (row, col, input, endRow, endCol, source, method, direction, deltas) { + var c; + + 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 + } + c = typeof endRow === 'number' ? new WalkontableCellCoords(endRow, endCol) : null; + + return grid.populateFromArray(new WalkontableCellCoords(row, col), input, c, source, method, direction, deltas); + }; + + /** + * 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/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/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 (Array.isArray(priv.settings.dataSchema) || Array.isArray(data[0])) { + 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' || Array.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') { + /* jshint -W089 */ + 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) { + Handsontable.Dom.removeClass(instance.rootElement,GridSettings.prototype.className); +// instance.rootElement.removeClass(GridSettings.prototype.className); + } + if (settings.className) { + Handsontable.Dom.addClass(instance.rootElement,settings.className); +// instance.rootElement.addClass(settings.className); + } + } + + if (typeof settings.height != 'undefined'){ + var height = settings.height; + + if (typeof height == 'function'){ + height = height(); + } + + instance.rootElement.style.height = height + 'px'; + } + + if (typeof settings.width != 'undefined'){ + var width = settings.width; + + if (typeof width == 'function'){ + width = width(); + } + + instance.rootElement.style.width = width + 'px'; + } + + /* jshint ignore:start */ + if (height){ + instance.rootElement.style.overflow = 'auto'; + } + /* jshint ignore:end */ + + 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')) { + // ignore obj.prototype.type + return; + } + + 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; + }; + + /** + * Returns end of the selection in text input + * @return {Number} + */ + Handsontable.Dom.getSelectionEndPosition = function (el) { + if(el.selectionEnd) { + return el.selectionEnd; + } else if(document.selection) { //IE8 + var r = document.selection.createRange(); + if(r == null) { + return 0; + } + var re = el.createTextRange(); + + return re.text.indexOf(r.text) + r.text.length; + } + }; + + /** + * 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; + }; + + var isIE8 = !(document.createTextNode('test').textContent); + Handsontable.Dom.isIE8 = function () { + return isIE8; + }; + + var isIE9 = !!(document.documentMode); + Handsontable.Dom.isIE9 = function () { + return isIE9; + }; + + var isSafari = (/Safari/.test(navigator.userAgent) && /Apple Computer/.test(navigator.vendor)); + Handsontable.Dom.isSafari = function () { + return isSafari; + }; + + /** + * Sets overlay position depending on it's type and used browser + */ + Handsontable.Dom.setOverlayPosition = function (overlayElem, left, top) { + if (isIE8 || isIE9) { + overlayElem.style.top = top; + overlayElem.style.left = left; + } else if (isSafari) { + overlayElem.style['-webkit-transform'] = 'translate3d(' + left + ',' + top + ',0)'; + } else { + overlayElem.style['transform'] = 'translate3d(' + left + ',' + top + ',0)'; + } + }; + + Handsontable.Dom.getCssTransform = function (elem) { + var transform; + + /* jshint ignore:start */ + if(elem.style['transform'] && (transform = elem.style['transform']) != "") { + return ['transform', transform]; + } else if (elem.style['-webkit-transform'] && (transform = elem.style['-webkit-transform']) != "") { + return ['-webkit-transform', transform]; + } else { + return -1; + } + /* jshint ignore:end */ + }; + + Handsontable.Dom.resetCssTransform = function (elem) { + /* jshint ignore:start */ + if(elem['transform'] && elem['transform'] != "") { + elem['transform'] = ""; + } else if(elem['-webkit-transform'] && elem['-webkit-transform'] != "") { + elem['-webkit-transform'] = ""; + } + /* jshint ignore:end */ + }; + +})(); + + +// if(!window.Handsontable){ +// Handsontable = {}; +// } + +Handsontable.countEventManagerListeners = 0; //used to debug memory leaks + +Handsontable.eventManager = function (instance) { + var + addEvent, + removeEvent, + clearEvents, + fireEvent; + + if (!instance) { + throw new Error ('instance not defined'); + } + if (!instance.eventListeners) { + instance.eventListeners = []; + } + + /** + * Add Event + * + * @param {Element} element + * @param {String} event + * @param {Function} callback + * @returns {Function} Returns function which you can easily call to remove that event + */ + addEvent = function (element, event, callback) { + var callbackProxy; + + callbackProxy = function (event) { + if (event.target == void 0 && event.srcElement != void 0) { + if (event.definePoperty) { + event.definePoperty('target', { + value: event.srcElement + }); + } else { + event.target = event.srcElement; + } + } + + if (event.preventDefault == void 0) { + if (event.definePoperty) { + event.definePoperty('preventDefault', { + value: function() { + this.returnValue = false; + } + }); + } else { + event.preventDefault = function () { + this.returnValue = false; + }; + } + } + callback.call(this, event); + }; + + instance.eventListeners.push({ + element: element, + event: event, + callback: callback, + callbackProxy: callbackProxy + }); + + if (window.addEventListener) { + element.addEventListener(event, callbackProxy, false); + } else { + element.attachEvent('on' + event, callbackProxy); + } + Handsontable.countEventManagerListeners ++; + + return function _removeEvent() { + removeEvent(element, event, callback); + }; + }; + + /** + * Remove event + * + * @param {Element} element + * @param {String} event + * @param {Function} callback + */ + removeEvent = function (element, event, callback) { + var len = instance.eventListeners.length, + tmpEvent; + + while (len--) { + tmpEvent = instance.eventListeners[len]; + + if (tmpEvent.event == event && tmpEvent.element == element) { + if (callback && callback != tmpEvent.callback) { + continue; + } + instance.eventListeners.splice(len, 1); + + if (tmpEvent.element.removeEventListener) { + tmpEvent.element.removeEventListener(tmpEvent.event, tmpEvent.callbackProxy, false); + } else { + tmpEvent.element.detachEvent('on' + tmpEvent.event, tmpEvent.callbackProxy); + } + Handsontable.countEventManagerListeners --; + } + } + }; + + /** + * Clear all events + */ + clearEvents = function () { + var len = instance.eventListeners.length, + event; + + while (len--) { + event = instance.eventListeners[len]; + + if (event) { + removeEvent(event.element, event.event, event.callback); + } + } + }; + + /** + * Trigger event + * + * @param {Element} element + * @param {String} type + */ + fireEvent = function (element, type) { + var options, event; + + options = { + bubbles: true, + cancelable: (type !== "mousemove"), + view: window, + detail: 0, + screenX: 0, + screenY: 0, + clientX: 1, + clientY: 1, + ctrlKey: false, + altKey: false, + shiftKey: false, + metaKey: false, + button: 0, + relatedTarget: undefined + }; + + if (document.createEvent) { + event = document.createEvent("MouseEvents"); + event.initMouseEvent(type, options.bubbles, options.cancelable, + options.view, options.detail, + options.screenX, options.screenY, options.clientX, options.clientY, + options.ctrlKey, options.altKey, options.shiftKey, options.metaKey, + options.button, options.relatedTarget || document.body.parentNode); + + } else { + event = document.createEventObject(); + } + + if (element.dispatchEvent) { + element.dispatchEvent(event); + } else { + element.fireEvent('on' + type, event); + } + }; + + return { + addEventListener: addEvent, + removeEventListener: removeEvent, + clear: clearEvents, + fireEvent: fireEvent + }; +}; + +/** + * Handsontable TableView constructor + * @param {Object} instance + */ +Handsontable.TableView = function (instance) { + var that = this; + + this.eventManager = Handsontable.eventManager(instance); + this.instance = instance; + this.settings = instance.getSettings(); + + + var originalStyle = instance.rootElement.getAttribute('style'); + if(originalStyle) { + instance.rootElement.setAttribute('data-originalstyle', originalStyle); //needed to retrieve original style in jsFiddle link generator in HT examples. may be removed in future versions + } + + Handsontable.Dom.addClass(instance.rootElement,'handsontable'); +// 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.insertBefore(table, instance.container.firstChild); + + this.eventManager.addEventListener(instance.rootElement,'mousedown', 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. + } + }); + + this.eventManager.addEventListener(document.documentElement, 'keyup',function (event) { + if (instance.selection.isInProgress() && !event.shiftKey) { + instance.selection.finish(); + } + }); + + var isMouseDown; + this.isMouseDown = function () { + return isMouseDown; + }; + + this.eventManager.addEventListener(document.documentElement, 'mouseup', 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(); + } + }); + + this.eventManager.addEventListener(document.documentElement, 'mousedown',function (event) { + var next = event.target; + + if (isMouseDown) { + return; //it must have been started in a cell + } + + 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) { + 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(); + } + }); + + + + this.eventManager.addEventListener(table, 'selectstart', function (event) { + if (that.settings.fragmentSelection) { + return; + } + + //https://github.com/handsontable/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 selections = [ + new WalkontableSelection({ + className: 'current', + border: { + width: 2, + color: '#5292F7', + //style: 'solid', //not used + cornerVisible: function () { + return that.settings.fillHandle && !that.isCellEdited() && !instance.selection.isMultiple(); + }, + multipleSelectionHandlesVisible: function () { + return !that.isCellEdited() && !instance.selection.isMultiple(); + } + } + }), + new WalkontableSelection({ + className: 'area', + border: { + width: 1, + color: '#89AFF9', + //style: 'solid', // not used + cornerVisible: function () { + return that.settings.fillHandle && !that.isCellEdited() && instance.selection.isMultiple(); + }, + multipleSelectionHandlesVisible: function () { + return !that.isCellEdited() && instance.selection.isMultiple(); + } + } + }), + new WalkontableSelection({ + className: 'highlight', + highlightRowClassName: that.settings.currentRowClassName, + highlightColumnClassName: that.settings.currentColClassName + }), + new WalkontableSelection({ + className: 'fill', + border: { + width: 1, + color: 'red' + //style: 'solid' // not used + } + }) + ]; + selections.current = selections[0]; + selections.area = selections[1]; + selections.highlight = selections[2]; + selections.fill = selections[3]; + + var walkontableConfig = { + debug: function () { + return that.settings.debug; + }, + table: table, + stretchH: this.settings.stretchH, + data: instance.getDataAtCell, + totalRows: instance.countRows, + totalColumns: instance.countCols, + fixedColumnsLeft: function () { + return that.settings.fixedColumnsLeft; + }, + fixedRowsTop: function () { + return that.settings.fixedRowsTop; + }, + renderAllRows: that.settings.renderAllRows, + rowHeaders: function () { + var arr = []; + if(instance.hasRowHeaders()) { + arr.push(function (index, TH) { + that.appendRowHeader(index, TH); + }); + } + Handsontable.hooks.run(instance, 'afterGetRowHeaderRenderers', arr); + return arr; + }, + columnHeaders: function () { + + var arr = []; + if(instance.hasColHeaders()) { + arr.push(function (index, TH) { + that.appendColHeader(index, TH); + }); + } + Handsontable.hooks.run(instance, 'afterGetColumnHeaderRenderers', arr); + return arr; + }, + 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: selections, + hideBorderOnMouseDownOver: function () { + return that.settings.fragmentSelection; + }, + onCellMouseDown: function (event, coords, TD, wt) { + instance.listen(); + that.activeWt = wt; + + isMouseDown = true; + + Handsontable.hooks.run(instance, 'beforeOnCellMouseDown', event, coords, TD); + + Handsontable.Dom.enableImmediatePropagation(event); + + if (!event.isImmediatePropagationStopped()) { + + 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); + instance.selection.setSelectedHeaders(false, true); + } + if (coords.col < 0) { + instance.selectCell(coords.row, 0, coords.row, instance.countCols() - 1); + instance.selection.setSelectedHeaders(true, false); + } + } + 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); + } + } else { + if (isMouseDown) { + // multi select columns + if (coords.row < 0) { + instance.selection.setRangeEnd(new WalkontableCellCoords(instance.countRows() - 1, coords.col)); + instance.selection.setSelectedHeaders(false, true); + } + + // multi select rows + if (coords.col < 0) { + instance.selection.setRangeEnd(new WalkontableCellCoords(coords.row, instance.countCols() - 1)); + instance.selection.setSelectedHeaders(true, false); + } + } + } + + 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'); + }, + onBeforeDrawBorders: function (corners, borderClassName) { + instance.runHooks('beforeDrawBorders', corners, borderClassName); + }, + onBeforeTouchScroll: function () { + instance.runHooks('beforeTouchScroll'); + }, + onAfterMomentumScroll: function () { + instance.runHooks('afterMomentumScroll'); + }, + viewportRowCalculatorOverride: function (calc) { + if (that.settings.viewportRowRenderingOffset) { + calc.startRow = Math.max(calc.startRow - that.settings.viewportRowRenderingOffset, 0); + calc.endRow = Math.min(calc.endRow + that.settings.viewportRowRenderingOffset, instance.countRows() - 1); + } + instance.runHooks('afterViewportRowCalculatorOverride', calc); + }, + viewportColumnCalculatorOverride: function (calc) { + if (that.settings.viewportColumnRenderingOffset) { + calc.startColumn = Math.max(calc.startColumn - that.settings.viewportColumnRenderingOffset, 0); + calc.endColumn = Math.min(calc.endColumn + that.settings.viewportColumnRenderingOffset, instance.countCols() - 1); + } + instance.runHooks('afterViewportColumnCalculatorOverride', calc); + } + }; + + Handsontable.hooks.run(instance, 'beforeInitWalkontable', walkontableConfig); + + this.wt = new Walkontable(walkontableConfig); + this.activeWt = this.wt; + + this.eventManager.addEventListener(that.wt.wtTable.spreader, 'mousedown', 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 + Handsontable.helper.stopPropagation(event); + //event.stopPropagation(); + } + }); + + this.eventManager.addEventListener(that.wt.wtTable.spreader, 'contextmenu', 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 + Handsontable.helper.stopPropagation(event); + //event.stopPropagation(); + } + }); + + + this.eventManager.addEventListener(document.documentElement, 'click', 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.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 + * @param {Boolean} topmost + */ +Handsontable.TableView.prototype.getCellAtCoords = function (coords, topmost) { + var td = this.wt.getCell(coords, topmost); + //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, String.fromCharCode(160)); // workaround for https://github.com/handsontable/handsontable/issues/1946 + } + + 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'; + + if (col > -1) { + Handsontable.Dom.fastInnerHTML(SPAN, this.instance.getColHeader(col)); + } else { + Handsontable.Dom.fastInnerText(SPAN, String.fromCharCode(160)); // workaround for https://github.com/handsontable/handsontable/issues/1946 + } + 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} leftOffset + * @return {Number} + */ +Handsontable.TableView.prototype.maximumVisibleElementWidth = function (leftOffset) { + 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) { + 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; +}; + +Handsontable.TableView.prototype.destroy = function () { + this.wt.destroy(); + this.eventManager.clear(); +}; + +/** + * 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 keyCodes = Handsontable.helper.keyCode; + var destroyed = false; + + var eventManager = Handsontable.eventManager(instance); + + var activeEditor; + + var init = function () { + + function onKeyDown(event) { + + if (!instance.isListening()) { + return; + } + + Handsontable.hooks.run(instance, 'beforeKeyDown', event); + + if(destroyed) { + return; + } + + Handsontable.Dom.enableImmediatePropagation(event); + + 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(""); + return; + } + } + + var rangeModifier = event.shiftKey ? selection.setRangeEnd : selection.setRangeStart; + + switch (event.keyCode) { + + case keyCodes.A: + if (ctrlDown) { + selection.selectAll(); //select all cells + + event.preventDefault(); + Handsontable.helper.stopPropagation(event); + //event.stopPropagation(); + } + break; + + case keyCodes.ARROW_UP: + + if (that.isEditorOpened() && !activeEditor.isWaiting()){ + that.closeEditorAndSaveChanges(ctrlDown); + } + + moveSelectionUp(event.shiftKey); + + event.preventDefault(); + Handsontable.helper.stopPropagation(event); + //event.stopPropagation(); //required by HandsontableEditor + break; + + case keyCodes.ARROW_DOWN: + if (that.isEditorOpened() && !activeEditor.isWaiting()){ + that.closeEditorAndSaveChanges(ctrlDown); + } + + moveSelectionDown(event.shiftKey); + + event.preventDefault(); + Handsontable.helper.stopPropagation(event); + //event.stopPropagation(); //required by HandsontableEditor + break; + + case keyCodes.ARROW_RIGHT: + if(that.isEditorOpened() && !activeEditor.isWaiting()){ + that.closeEditorAndSaveChanges(ctrlDown); + } + + moveSelectionRight(event.shiftKey); + + event.preventDefault(); + Handsontable.helper.stopPropagation(event); + //event.stopPropagation(); //required by HandsontableEditor + break; + + case keyCodes.ARROW_LEFT: + if(that.isEditorOpened() && !activeEditor.isWaiting()){ + that.closeEditorAndSaveChanges(ctrlDown); + } + + moveSelectionLeft(event.shiftKey); + + event.preventDefault(); + Handsontable.helper.stopPropagation(event); + //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(); + Handsontable.helper.stopPropagation(event); + //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 + Handsontable.helper.stopPropagation(event); + //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 + Handsontable.helper.stopPropagation(event); + //event.stopPropagation(); //required by HandsontableEditor + break; + + case keyCodes.PAGE_UP: + selection.transformStart(-instance.countVisibleRows(), 0); + event.preventDefault(); //don't page up the window + Handsontable.helper.stopPropagation(event); + //event.stopPropagation(); //required by HandsontableEditor + break; + + case keyCodes.PAGE_DOWN: + selection.transformStart(instance.countVisibleRows(), 0); + event.preventDefault(); //don't page down the window + Handsontable.helper.stopPropagation(event); + //event.stopPropagation(); //required by HandsontableEditor + break; + } + + } + } + } + + instance.addHook('afterDocumentKeyDown', function(originalEvent){ + onKeyDown(originalEvent); + }); + + eventManager.addEventListener(document, 'keydown', function (ev){ + instance.runHooks('afterDocumentKeyDown', ev); + }); + + function onDblClick(event, coords, elem) { + if(elem.nodeName == "TD") { //may be TD or TH + 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); + +Handsontable.helper = {}; + +/** + * 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 + ''; + + case 'object': + if (value === null) { + return ''; + } + else { + return value.toString(); + } + break; + case 'undefined': + return ''; + + 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; +}; + +/** + * Creates 2D array of Excel-like values "A1", "A2", ... + * @param rowCount + * @param colCount + * @returns {Array} + */ +Handsontable.helper.createSpreadsheetData = function(rowCount, colCount) { + rowCount = typeof rowCount === 'number' ? rowCount : 100; + colCount = typeof colCount === 'number' ? colCount : 4; + + var rows = [] + , i + , j; + + for (i = 0; i < rowCount; i++) { + var row = []; + for (j = 0; j < colCount; j++) { + row.push(Handsontable.helper.spreadsheetColumnLabel(j) + (i + 1)); + } + rows.push(row); + } + return rows; +}; + +Handsontable.helper.createSpreadsheetObjectData = function(rowCount, colCount) { + rowCount = typeof rowCount === 'number' ? rowCount : 100; + colCount = typeof colCount === 'number' ? colCount : 4; + + var rows = [] + , i + , j; + + for (i = 0; i < rowCount; i++) { + var row = {}; + for (j = 0; j < colCount; j++) { + row['prop' + j] = Handsontable.helper.spreadsheetColumnLabel(j) + (i + 1); + } + rows.push(row); + } + return rows; +}; + +/** + * 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; +}; + +/** + * 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/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]; + } + } +}; + +/** + * Perform deep 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.deepExtend = function (target, extension) { + for (var key in extension) { + if (extension.hasOwnProperty(key)) { + if (extension[key] && typeof extension[key] === 'object') { + if (!target[key]) { + if (Array.isArray(extension[key])) { + target[key] = []; + } + else { + target[key] = {}; + } + } + Handsontable.helper.deepExtend(target[key], extension[key]); + } + else { + target[key] = extension[key]; + } + } + } +}; + +/** + * Perform deep clone of an object + * WARNING! Only clones JSON properties. Will cause error when `obj` contains a function, Date, etc + * @param {Object} obj An object that will be cloned + * @return {Object} + */ +Handsontable.helper.deepClone = function (obj) { + if (typeof obj === "object") { + return JSON.parse(JSON.stringify(obj)); + } + else { + return obj; + } +}; + +Handsontable.helper.getPrototypeOf = function (obj) { + var prototype; + + /* jshint ignore:start */ + 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 + + } + /* jshint ignore:end */ + + 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, + INSERT: 45, + 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]'; +}; + +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.isMobileBrowser = function (userAgent) { + if(!userAgent) { + userAgent = navigator.userAgent; + } + return (/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(userAgent)); + + // Logic for checking the specific mobile browser + // + /* var type = type != void 0 ? type.toLowerCase() : '' + , result; + switch(type) { + case '': + result = (/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)); + return result; + break; + case 'ipad': + return navigator.userAgent.indexOf('iPad') > -1; + break; + case 'android': + return navigator.userAgent.indexOf('Android') > -1; + break; + case 'windows': + return navigator.userAgent.indexOf('IEMobile') > -1; + break; + default: + throw new Error('Invalid isMobileBrowser argument'); + break; + } */ +}; + +Handsontable.helper.isTouchSupported = function () { + return ('ontouchstart' in window); +}; + +Handsontable.helper.stopPropagation = function (event) { + // ie8 + //http://msdn.microsoft.com/en-us/library/ie/ff975462(v=vs.85).aspx + if (typeof (event.stopPropagation) === 'function') { + event.stopPropagation(); + } + else { + event.cancelBubble = true; + } +}; + +Handsontable.helper.pageX = function (event) { + if (event.pageX) { + return event.pageX; + } + + var scrollLeft = Handsontable.Dom.getWindowScrollLeft(); + var cursorX = event.clientX + scrollLeft; + + return cursorX; +}; + +Handsontable.helper.pageY = function (event) { + if (event.pageY) { + return event.pageY; + } + + var scrollTop = Handsontable.Dom.getWindowScrollTop(); + var cursorY = event.clientY + scrollTop; + + return cursorY; +}; + +(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 (!Array.isArray(obj)){ + schema = {}; + for (var i in obj) { + if (obj.hasOwnProperty(i)) { + if (typeof obj[i] === "object" && !Array.isArray(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 (typeof schema === "object" && !Array.isArray(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 () { + var i, ilen, schema = this.getSchema(); + if (typeof schema === "undefined") { + throw new Error("trying to create `columns` definition but you didnt' provide `schema` nor `data`"); + } + 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.run(this.instance, 'modifyCol', col); + + if (this.colToPropCache && typeof this.colToPropCache[col] !== 'undefined') { + return this.colToPropCache[col]; + } + + 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.run(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 = {}; + Handsontable.helper.deepExtend(row, 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.run(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.run(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.run(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.run(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++) { + + if (typeof out[sliced[i]] === 'undefined'){ + out[sliced[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.run(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 properties (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/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(String.fromCharCode(9660))); // workaround for https://github.com/handsontable/handsontable/issues/1946 +//this is faster than innerHTML. See: https://github.com/handsontable/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(String.fromCharCode(160))); // workaround for https://github.com/handsontable/handsontable/issues/1946 + //this is faster than innerHTML. See: https://github.com/handsontable/handsontable/wiki/JavaScript-&-DOM-performance-tips + } + + + + if (!instance.acArrowListener) { + var eventManager = Handsontable.eventManager(instance); + + //not very elegant but easy and fast + instance.acArrowListener = function (event) { + if (Handsontable.Dom.hasClass(event.target,'htAutocompleteArrow')) { + instance.view.wt.getSetting('onCellDblClick', null, new WalkontableCellCoords(row, col), TD); + } + }; + + eventManager.addEventListener(instance.rootElement,'mousedown',instance.acArrowListener); + + //We need to unbind the listener after the table has been destroyed + instance.addHookOnce('afterDestroy', function () { + eventManager.clear(); + }); + + } + }; + + 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) { + + var eventManager = Handsontable.eventManager(instance); + + 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/handsontable/wiki/JavaScript-&-DOM-performance-tips + } + + if (cellProperties.readOnly) { + eventManager.addEventListener(INPUT,'click',function (event) { + event.preventDefault(); + }); + } + else { + eventManager.addEventListener(INPUT,'mousedown',function (event) { + Handsontable.helper.stopPropagation(event); + //event.stopPropagation(); //otherwise can confuse cell mousedown handler + }); + + eventManager.addEventListener(INPUT,'mouseup',function (event) { + Handsontable.helper.stopPropagation(event); + //event.stopPropagation(); //otherwise can confuse cell dblclick handler + }); + + eventManager.addEventListener(INPUT,'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){ + + Handsontable.Dom.enableImmediatePropagation(event); + + if(event.keyCode == Handsontable.helper.keyCode.SPACE || event.keyCode == Handsontable.helper.keyCode.ENTER){ + + 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; + eventManager.fireEvent(checkbox[i], '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 properties (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(Handsontable){ + + '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); + + }; + + Handsontable.PasswordRenderer = PasswordRenderer; + Handsontable.renderers.PasswordRenderer = PasswordRenderer; + Handsontable.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() + , tmp; + + if(sel[0] > sel[2]) { + tmp = sel[0]; + sel[0] = sel[2]; + sel[2] = tmp; + } + if(sel[1] > sel[3]) { + tmp = sel[1]; + sel[1] = sel[3]; + sel[3] = tmp; + } + + 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) { + var _this = this; + + 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) { + this.instance._registerTimeout(setTimeout(function () { + _this._fireCallbacks(true); + }, 0)); + + return; + } + + if (this.state == Handsontable.EditorState.EDITING) { + if (restoreOriginalValue) { + this.cancelChanges(); + this.instance.view.render(); + + 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)) { + this.instance.addHookOnce('postAfterValidate', function (result) { + _this.state = Handsontable.EditorState.FINISHED; + _this.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; + } + // validator was defined and failed + if (result === false && this.cellProperties.allowInvalid !== true) { + 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(){ + var that = this; + this.createElements(); + this.eventManager = new Handsontable.eventManager(this); + this.bindEvents(); + this.autoResize = autoResize(); + + this.instance.addHook('afterDestroy', function () { + that.destroy(); + }); + }; + + 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) + + Handsontable.Dom.enableImmediatePropagation(event); + + //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; + } + + that.autoResize.resize(String.fromCharCode(event.keyCode)); + }; + + + + TextEditor.prototype.open = function(){ + this.refreshDimensions(); //need it instantly, to prevent https://github.com/handsontable/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'); + + 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.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); + 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(); + var cssTransformOffset; + + // TODO: Refactor this to the new instance.getCell method (from #ply-59), after 0.12.1 is released + switch(editorSection) { + case 'top': + cssTransformOffset = Handsontable.Dom.getCssTransform(this.instance.view.wt.wtScrollbars.vertical.clone.wtTable.holder.parentNode); + break; + case 'left': + cssTransformOffset = Handsontable.Dom.getCssTransform(this.instance.view.wt.wtScrollbars.horizontal.clone.wtTable.holder.parentNode); + break; + case 'corner': + cssTransformOffset = Handsontable.Dom.getCssTransform(this.instance.view.wt.wtScrollbars.corner.clone.wtTable.holder.parentNode); + break; + } + + if (editTop < 0) { + editTop = 0; + } + if (editLeft < 0) { + editLeft = 0; + } + if (rowHeadersCount > 0 && parseInt(this.TD.style.borderTopWidth, 10) > 0) { + editTop += 1; + } + if (colHeadersCount > 0 && parseInt(this.TD.style.borderLeftWidth, 10) > 0) { + editLeft += 1; + } + + + if(cssTransformOffset && cssTransformOffset != -1) { + this.textareaParentStyle[cssTransformOffset[0]] = cssTransformOffset[1]; + } else { + Handsontable.Dom.resetCssTransform(this.textareaParentStyle); + } + + this.textareaParentStyle.top = editTop + 'px'; + this.textareaParentStyle.left = editLeft + 'px'; + + ///end prepare textarea position + + + var cellTopOffset = this.TD.offsetTop - this.instance.view.wt.wtScrollbars.vertical.getScrollPosition(), + cellLeftOffset = this.TD.offsetLeft - this.instance.view.wt.wtScrollbars.horizontal.getScrollPosition(); + + var width = Handsontable.Dom.innerWidth(this.TD) - 8 //$td.width() + , maxWidth = this.instance.view.maximumVisibleElementWidth(cellLeftOffset) - 10 //10 is TEXTAREAs border and padding + , height = Handsontable.Dom.outerHeight(this.TD) - 4 //$td.outerHeight() - 4 + , maxHeight = this.instance.view.maximumVisibleElementHeight(cellTopOffset) - 2; //10 is TEXTAREAs border and padding + + if (parseInt(this.TD.style.borderTopWidth, 10) > 0) { + height -= 1; + } + if (parseInt(this.TD.style.borderLeftWidth, 10) > 0) { + if (rowHeadersCount > 0) { + width -= 1; + } + } + + this.TEXTAREA.style.fontSize = Handsontable.Dom.getComputedStyle(this.TD).fontSize; + this.TEXTAREA.style.fontFamily = Handsontable.Dom.getComputedStyle(this.TD).fontFamily; + + this.autoResize.init(this.TEXTAREA, { + 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) + }, true); + + this.textareaParentStyle.display = 'block'; + }; + + TextEditor.prototype.bindEvents = function () { + var editor = this; + + this.eventManager.addEventListener(this.TEXTAREA, 'cut',function (event){ + Handsontable.helper.stopPropagation(event); + //event.stopPropagation(); + }); + + this.eventManager.addEventListener(this.TEXTAREA, 'paste', function (event){ + Handsontable.helper.stopPropagation(event); + //event.stopPropagation(); + }); + + this.instance.addHook('afterScrollVertically', function () { + editor.refreshDimensions(); + }); + + this.instance.addHook('afterColumnResize', function () { + editor.refreshDimensions(); + editor.focus(); + }); + + this.instance.addHook('afterRowResize', function () { + editor.refreshDimensions(); + editor.focus(); + }); + + this.instance.addHook('afterDestroy', function () { + editor.eventManager.clear(); + }); + }; + + TextEditor.prototype.destroy = function () { + this.eventManager.clear(); + }; + + + Handsontable.editors.TextEditor = TextEditor; + Handsontable.editors.registerEditor('text', Handsontable.editors.TextEditor); + +})(Handsontable); + +(function (Handsontable) { + + var MobileTextEditor = Handsontable.editors.BaseEditor.prototype.extend(); + + var domDimensionsCache = {}; + + var createControls = function () { + this.controls = {}; + + this.controls.leftButton = document.createElement('DIV'); + this.controls.leftButton.className = 'leftButton'; + this.controls.rightButton = document.createElement('DIV'); + this.controls.rightButton.className = 'rightButton'; + this.controls.upButton = document.createElement('DIV'); + this.controls.upButton.className = 'upButton'; + this.controls.downButton = document.createElement('DIV'); + this.controls.downButton.className = 'downButton'; + + for (var button in this.controls) { + if (this.controls.hasOwnProperty(button)) { + this.positionControls.appendChild(this.controls[button]); + } + } + }; + + MobileTextEditor.prototype.valueChanged = function () { + return this.initValue != this.getValue(); + }; + + MobileTextEditor.prototype.init = function () { + var that = this; + this.eventManager = new Handsontable.eventManager(this.instance); + + this.createElements(); + this.bindEvents(); + + this.instance.addHook('afterDestroy', function () { + that.destroy(); + }); + + }; + + MobileTextEditor.prototype.getValue = function () { + return this.TEXTAREA.value; + }; + + MobileTextEditor.prototype.setValue = function (newValue) { + this.initValue = newValue; + + this.TEXTAREA.value = newValue; + }; + + MobileTextEditor.prototype.createElements = function () { + this.editorContainer = document.createElement('DIV'); + this.editorContainer.className = "htMobileEditorContainer"; + + this.cellPointer = document.createElement('DIV'); + this.cellPointer.className = "cellPointer"; + + this.moveHandle = document.createElement('DIV'); + this.moveHandle.className = "moveHandle"; + + this.inputPane = document.createElement('DIV'); + this.inputPane.className = "inputs"; + + this.positionControls = document.createElement('DIV'); + this.positionControls.className = "positionControls"; + + this.TEXTAREA = document.createElement('TEXTAREA'); + Handsontable.Dom.addClass(this.TEXTAREA, 'handsontableInput'); + + this.inputPane.appendChild(this.TEXTAREA); + + this.editorContainer.appendChild(this.cellPointer); + this.editorContainer.appendChild(this.moveHandle); + this.editorContainer.appendChild(this.inputPane); + this.editorContainer.appendChild(this.positionControls); + + createControls.call(this); + + document.body.appendChild(this.editorContainer); + }; + + MobileTextEditor.prototype.onBeforeKeyDown = function (event) { + var instance = this; + var that = instance.getActiveEditor(); + + Handsontable.Dom.enableImmediatePropagation(event); + + if (event.target !== that.TEXTAREA || event.isImmediatePropagationStopped()){ + return; + } + + var keyCodes = Handsontable.helper.keyCode; + + switch(event.keyCode) { + case keyCodes.ENTER: + that.close(); + event.preventDefault(); //don't add newline to field + break; + case keyCodes.BACKSPACE: + event.stopImmediatePropagation(); //backspace, delete, home, end should only work locally when cell is edited (not in table context) + break; + } + }; + + MobileTextEditor.prototype.open = function () { + this.instance.addHook('beforeKeyDown', this.onBeforeKeyDown); + + Handsontable.Dom.addClass(this.editorContainer, 'active'); + //this.updateEditorDimensions(); + //this.scrollToView(); + Handsontable.Dom.removeClass(this.cellPointer, 'hidden'); + + this.updateEditorPosition(); + }; + + MobileTextEditor.prototype.focus = function(){ + this.TEXTAREA.focus(); + Handsontable.Dom.setCaretPosition(this.TEXTAREA, this.TEXTAREA.value.length); + }; + + MobileTextEditor.prototype.close = function () { + this.TEXTAREA.blur(); + this.instance.removeHook('beforeKeyDown', this.onBeforeKeyDown); + + Handsontable.Dom.removeClass(this.editorContainer, 'active'); + }; + + MobileTextEditor.prototype.scrollToView = function () { + var coords = this.instance.getSelectedRange().highlight; + this.instance.view.scrollViewport(coords); + }; + + MobileTextEditor.prototype.hideCellPointer = function () { + if(!Handsontable.Dom.hasClass(this.cellPointer, 'hidden')) { + Handsontable.Dom.addClass(this.cellPointer, 'hidden'); + } + }; + + MobileTextEditor.prototype.updateEditorPosition = function (x, y) { + if(x && y) { + x = parseInt(x, 10); + y = parseInt(y, 10); + + this.editorContainer.style.top = y + "px"; + this.editorContainer.style.left = x + "px"; + + } else { + var selection = this.instance.getSelected() + , selectedCell = this.instance.getCell(selection[0],selection[1]); + + //cache sizes + if(!domDimensionsCache.cellPointer) { + domDimensionsCache.cellPointer = { + height: Handsontable.Dom.outerHeight(this.cellPointer), + width: Handsontable.Dom.outerWidth(this.cellPointer) + }; + } + if(!domDimensionsCache.editorContainer) { + domDimensionsCache.editorContainer = { + width: Handsontable.Dom.outerWidth(this.editorContainer) + }; + } + + if(selectedCell !== undefined) { + var scrollLeft = this.instance.view.wt.wtScrollbars.horizontal.scrollHandler == window ? + 0 : Handsontable.Dom.getScrollLeft(this.instance.view.wt.wtScrollbars.horizontal.scrollHandler); + var scrollTop = this.instance.view.wt.wtScrollbars.vertical.scrollHandler == window ? + 0 : Handsontable.Dom.getScrollTop(this.instance.view.wt.wtScrollbars.vertical.scrollHandler); + + var selectedCellOffset = Handsontable.Dom.offset(selectedCell) + , selectedCellWidth = Handsontable.Dom.outerWidth(selectedCell) + , currentScrollPosition = { + x: scrollLeft, + y: scrollTop + }; + + this.editorContainer.style.top = parseInt(selectedCellOffset.top + Handsontable.Dom.outerHeight(selectedCell) - + currentScrollPosition.y + domDimensionsCache.cellPointer.height, 10) + "px"; + this.editorContainer.style.left = parseInt((window.innerWidth / 2) - + (domDimensionsCache.editorContainer.width / 2) ,10) + "px"; + + if(selectedCellOffset.left + selectedCellWidth / 2 > parseInt(this.editorContainer.style.left,10) + domDimensionsCache.editorContainer.width) { + this.editorContainer.style.left = window.innerWidth - domDimensionsCache.editorContainer.width + "px"; + } else if(selectedCellOffset.left + selectedCellWidth / 2 < parseInt(this.editorContainer.style.left,10) + 20) { + this.editorContainer.style.left = 0 + "px"; + } + + this.cellPointer.style.left = parseInt(selectedCellOffset.left - (domDimensionsCache.cellPointer.width / 2) - + Handsontable.Dom.offset(this.editorContainer).left + (selectedCellWidth / 2) - currentScrollPosition.x ,10) + "px"; + } + } + }; + + + // For the optional dont-affect-editor-by-zooming feature: + + //MobileTextEditor.prototype.updateEditorDimensions = function () { + // if(!this.beginningWindowWidth) { + // this.beginningWindowWidth = window.innerWidth; + // this.beginningEditorWidth = Handsontable.Dom.outerWidth(this.editorContainer); + // this.scaleRatio = this.beginningEditorWidth / this.beginningWindowWidth; + // + // this.editorContainer.style.width = this.beginningEditorWidth + "px"; + // return; + // } + // + // var currentScaleRatio = this.beginningEditorWidth / window.innerWidth; + // //if(currentScaleRatio > this.scaleRatio + 0.2 || currentScaleRatio < this.scaleRatio - 0.2) { + // if(currentScaleRatio != this.scaleRatio) { + // this.editorContainer.style["zoom"] = (1 - ((currentScaleRatio * this.scaleRatio) - this.scaleRatio)) * 100 + "%"; + // } + // + //}; + + MobileTextEditor.prototype.updateEditorData = function () { + var selected = this.instance.getSelected() + , selectedValue = this.instance.getDataAtCell(selected[0], selected[1]); + + this.row = selected[0]; + this.col = selected[1]; + this.setValue(selectedValue); + this.updateEditorPosition(); + }; + + MobileTextEditor.prototype.prepareAndSave = function () { + + if(!this.valueChanged()) { + return true; + } + + var val = [ + [String.prototype.trim.call(this.getValue())] + ]; + + this.saveValue(val); + }; + + MobileTextEditor.prototype.bindEvents = function () { + var that = this; + + this.eventManager.addEventListener(this.controls.leftButton, "touchend", function (event) { + that.prepareAndSave(); + that.instance.selection.transformStart(0, -1, null, true); + that.updateEditorData(); + event.preventDefault(); + }); + this.eventManager.addEventListener(this.controls.rightButton, "touchend", function (event) { + that.prepareAndSave(); + that.instance.selection.transformStart(0, 1, null, true); + that.updateEditorData(); + event.preventDefault(); + }); + this.eventManager.addEventListener(this.controls.upButton, "touchend", function (event) { + that.prepareAndSave(); + that.instance.selection.transformStart(-1, 0, null, true); + that.updateEditorData(); + event.preventDefault(); + }); + this.eventManager.addEventListener(this.controls.downButton, "touchend", function (event) { + that.prepareAndSave(); + that.instance.selection.transformStart(1, 0, null, true); + that.updateEditorData(); + event.preventDefault(); + }); + + this.eventManager.addEventListener(this.moveHandle, "touchstart", function (event) { + if (event.touches.length == 1) { + var touch = event.touches[0] + , onTouchPosition = { + x: that.editorContainer.offsetLeft, + y: that.editorContainer.offsetTop + } + , onTouchOffset = { + x: touch.pageX - onTouchPosition.x, + y: touch.pageY - onTouchPosition.y + }; + + that.eventManager.addEventListener(this, "touchmove", function (event) { + var touch = event.touches[0]; + that.updateEditorPosition(touch.pageX - onTouchOffset.x, touch.pageY - onTouchOffset.y); + that.hideCellPointer(); + event.preventDefault(); + }); + + } + }); + + this.eventManager.addEventListener(document.body, "touchend", function (event) { + if(!Handsontable.Dom.isChildOf(event.target, that.editorContainer) && !Handsontable.Dom.isChildOf(event.target, that.instance.rootElement)) { + that.close(); + } + }); + + this.eventManager.addEventListener(this.instance.view.wt.wtScrollbars.horizontal.scrollHandler, "scroll", function (event) { + if(that.instance.view.wt.wtScrollbars.horizontal.scrollHandler != window) { + that.hideCellPointer(); + } + }); + + this.eventManager.addEventListener(this.instance.view.wt.wtScrollbars.vertical.scrollHandler, "scroll", function (event) { + if(that.instance.view.wt.wtScrollbars.vertical.scrollHandler != window) { + that.hideCellPointer(); + } + }); + + }; + + MobileTextEditor.prototype.destroy = function () { + this.eventManager.clear(); + + this.editorContainer.parentNode.removeChild(this.editorContainer); + }; + + Handsontable.editors.MobileTextEditor = MobileTextEditor; + Handsontable.editors.registerEditor('mobile', Handsontable.editors.MobileTextEditor); + + + +})(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.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(); + + var $; + + DateEditor.prototype.init = function () { + if (typeof jQuery != 'undefined') { + $ = jQuery; + } else { + throw new Error("You need to include jQuery to your project in order to use the jQuery UI Datepicker."); + } + + 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); + + var eventManager = Handsontable.eventManager(this); + + /** + * Prevent recognizing clicking on jQuery Datepicker as clicking outside of table + */ + eventManager.addEventListener(this.datePicker, 'mousedown', function (event) { + Handsontable.helper.stopPropagation(event); + //event.stopPropagation(); + }); + + this.hideDatepicker(); + }; + + DateEditor.prototype.destroyElements = function () { + this.$datePicker.datepicker('destroy'); + this.$datePicker.remove(); + //var eventManager = Handsontable.eventManager(this); + //eventManager.removeEventListener(this.datePicker, 'mousedown'); + }; + + 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 offset = this.TD.getBoundingClientRect(), + DatepickerSettings, + datepickerSettings; + + this.datePickerStyle.top = (window.pageYOffset + offset.top + Handsontable.Dom.outerHeight(this.TD)) + 'px'; + this.datePickerStyle.left = (window.pageXOffset + offset.left) + 'px'; + + DatepickerSettings = function () {}; + DatepickerSettings.prototype = this.cellProperties; + datepickerSettings = new DatepickerSettings(); + datepickerSettings.defaultDate = this.originalValue || void 0; + this.$datePicker.datepicker('option', datepickerSettings); + + 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.htEditor = new Handsontable(DIV); + + this.assignHooks(); + }; + + 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(); + } + }; + + if (this.cellProperties.handsontable) { + Handsontable.helper.extend(options, cellProperties.handsontable); + } + if (this.htEditor) { + this.htEditor.destroy(); + } + + this.htEditor = new Handsontable(this.htContainer, options); + + //this.$htContainer.handsontable('destroy'); + //this.$htContainer.handsontable(options); + }; + + var onBeforeKeyDown = function (event) { + + if (event != null && event.isImmediatePropagationEnabled == null) { + event.stopImmediatePropagation = function () { + this.isImmediatePropagationEnabled = false; + this.cancelBubble = true; + }; + event.isImmediatePropagationEnabled = true; + event.isImmediatePropagationStopped = function () { + return !this.isImmediatePropagationEnabled; + }; + } + + if (event.isImmediatePropagationStopped()) { + return; + } + + var editor = this.getActiveEditor(); + + var innerHOT = editor.htEditor.getInstance(); //Handsontable.tmpHandsontable(editor.htContainer, 'getInstance'); + + var rowToSelect; + + if (event.keyCode == Handsontable.helper.keyCode.ARROW_DOWN) { + if (!innerHOT.getSelected()) { + rowToSelect = 0; + } + else { + var selectedRow = innerHOT.getSelected()[0]; + var lastRow = innerHOT.countRows() - 1; + rowToSelect = Math.min(lastRow, selectedRow + 1); + } + } + else if (event.keyCode == Handsontable.helper.keyCode.ARROW_UP) { + if (innerHOT.getSelected()) { + var selectedRow = innerHOT.getSelected()[0]; + rowToSelect = selectedRow - 1; + } + } + + if (rowToSelect !== void 0) { + if (rowToSelect < 0) { + innerHOT.deselectCell(); + } + else { + innerHOT.selectCell(rowToSelect, 0); + } + + event.preventDefault(); + event.stopImmediatePropagation(); + + editor.instance.listen(); + editor.TEXTAREA.focus(); + } + }; + + HandsontableEditor.prototype.open = function () { + + this.instance.addHook('beforeKeyDown', onBeforeKeyDown); + + Handsontable.editors.TextEditor.prototype.open.apply(this, arguments); + + //this.$htContainer.handsontable('render'); + + //Handsontable.tmpHandsontable(this.htContainer, 'render'); + this.htEditor.render(); + + if (this.cellProperties.strict) { + this.htEditor.selectCell(0,0); + this.TEXTAREA.style.visibility = 'hidden'; + } else { + this.htEditor.deselectCell(); + this.TEXTAREA.style.visibility = 'visible'; + } + + Handsontable.Dom.setCaretPosition(this.TEXTAREA, 0, this.TEXTAREA.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.htEditor.isListening()) { //if focus is still in the HOT editor + + //if (Handsontable.tmpHandsontable(this.htContainer,'isListening')) { //if focus is still in the HOT editor + //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.htEditor.getSelected()){ + //if (Handsontable.tmpHandsontable(this.htContainer,'getSelected')) { + //if (this.$htContainer.handsontable('getSelected')) { + // var value = this.$htContainer.handsontable('getInstance').getValue(); + var value = this.htEditor.getInstance().getValue(); + //var value = Handsontable.tmpHandsontable(this.htContainer,'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); + }; + + HandsontableEditor.prototype.assignHooks = function () { + var that = this; + this.instance.addHook('afterDestroy', function () { + if (that.htEditor) { + that.htEditor.destroy(); + } + }); + + }; + + 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); + + // set choices list initial height, so Walkontable can assign it's scroll handler + var choicesListHot = this.htEditor.getInstance(); + choicesListHot.updateSettings({ + height: 1 + }); + + this.query = null; + this.choices = []; + }; + + AutocompleteEditor.prototype.createElements = function(){ + Handsontable.editors.HandsontableEditor.prototype.createElements.apply(this, arguments); + + var getSystemSpecificPaddingClass = function () { + if(window.navigator.platform.indexOf('Mac') != -1) { + return "htMacScroll"; + } else { + return ""; + } + }; + + Handsontable.Dom.addClass(this.htContainer, 'autocompleteEditor'); + Handsontable.Dom.addClass(this.htContainer, getSystemSpecificPaddingClass()); + + }; + + var skipOne = false; + var onBeforeKeyDown = function (event) { + skipOne = false; + var editor = this.getActiveEditor(); + var keyCodes = Handsontable.helper.keyCode; + + if (Handsontable.helper.isPrintableChar(event.keyCode) || event.keyCode === keyCodes.BACKSPACE || event.keyCode === keyCodes.DELETE || event.keyCode === keyCodes.INSERT) { + var timeOffset = 0; + + // on ctl+c / cmd+c don't update suggestion list + if(event.keyCode === keyCodes.C && (event.ctrlKey || event.metaKey)) { + return; + } + + if(!editor.isOpened()) { + timeOffset += 10; + } + + editor.instance._registerTimeout(setTimeout(function () { + editor.queryChoices(editor.TEXTAREA.value); + skipOne = true; + }, timeOffset)); + } + }; + + AutocompleteEditor.prototype.prepare = function () { + this.instance.addHook('beforeKeyDown', onBeforeKeyDown); + Handsontable.editors.HandsontableEditor.prototype.prepare.apply(this, arguments); + }; + + AutocompleteEditor.prototype.open = function () { + Handsontable.editors.HandsontableEditor.prototype.open.apply(this, arguments); + + this.TEXTAREA.style.visibility = 'visible'; + this.focus(); + + this.htContainer.style.overflow = 'hidden'; // small hack to prevent vertical scrollbar causing a horizontal scrollbar + + var choicesListHot = this.htEditor.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; + + if(value){ + 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 + ''); + } + } + } + }); + + if(skipOne) { + skipOne = false; + } + that.instance._registerTimeout(setTimeout(function () { + that.queryChoices(that.TEXTAREA.value); + that.htContainer.style.overflow = 'auto'; // small hack to prevent vertical scrollbar causing a horizontal scrollbar + }, 0)); + + }; + + AutocompleteEditor.prototype.close = function () { + 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 (Array.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) { + var pos = Handsontable.Dom.getCaretPosition(this.TEXTAREA), + endPos = Handsontable.Dom.getSelectionEndPosition(this.TEXTAREA); + + var orderByRelevance = AutocompleteEditor.sortByRelevance(this.getValue(), choices, this.cellProperties.filteringCaseSensitive); + var highlightIndex; + + /* jshint ignore:start */ + if (this.cellProperties.filter != false) { + var sorted = []; + for(var i = 0, choicesCount = orderByRelevance.length; i < choicesCount; i++) { + sorted.push(choices[orderByRelevance[i]]); + } + highlightIndex = 0; + choices = sorted; + } + else { + highlightIndex = orderByRelevance[0]; + } + /* jshint ignore:end */ + + this.choices = choices; + + this.htEditor.loadData(Handsontable.helper.pivot([choices])); + this.htEditor.updateSettings({height: this.getDropdownHeight()}); + //Handsontable.tmpHandsontable(this.htContainer,'loadData', Handsontable.helper.pivot([choices])); + //Handsontable.tmpHandsontable(this.htContainer,'updateSettings', {height: this.getDropdownHeight()}); + + if (this.cellProperties.strict === true) { + this.highlightBestMatchingChoice(highlightIndex); + } + + this.instance.listen(); + this.TEXTAREA.focus(); + Handsontable.Dom.setCaretPosition(this.TEXTAREA, pos, (pos != endPos ? endPos : void 0)); + }; + + AutocompleteEditor.prototype.finishEditing = function (restoreOriginalValue) { + if (!restoreOriginalValue) { + this.instance.removeHook('beforeKeyDown', onBeforeKeyDown); + } + Handsontable.editors.HandsontableEditor.prototype.finishEditing.apply(this, arguments); + }; + + AutocompleteEditor.prototype.highlightBestMatchingChoice = function (index) { + if (typeof index === "number") { + this.htEditor.selectCell(index, 0); + } else { + this.htEditor.deselectCell(); + } + }; + + /** + * Filters and sorts by relevance + * @param value + * @param choices + * @param caseSensitive + * @returns {Array} array of indexes in original choices array + */ + AutocompleteEditor.sortByRelevance = function(value, choices, caseSensitive) { + + var choicesRelevance = [] + , currentItem + , valueLength = value.length + , valueIndex + , charsLeft + , result = [] + , i + , choicesCount; + + if(valueLength === 0) { + for(i = 0, choicesCount = choices.length; i < choicesCount; i++) { + result.push(i); + } + return result; + } + + for(i = 0, choicesCount = choices.length; i < choicesCount; i++) { + currentItem = choices[i]; + + if(caseSensitive) { + valueIndex = currentItem.indexOf(value); + } else { + valueIndex = currentItem.toLowerCase().indexOf(value.toLowerCase()); + } + + + if(valueIndex == -1) { continue; } + charsLeft = currentItem.length - valueIndex - valueLength; + + choicesRelevance.push({ + baseIndex: i, + index: valueIndex, + charsLeft: charsLeft, + value: currentItem + }); + } + + choicesRelevance.sort(function(a, b) { + + if(b.index === -1) { + return -1; + } + if(a.index === -1) { + return 1; + } + + if(a.index < b.index) { + return -1; + } else if(b.index < a.index) { + return 1; + } else if(a.index === b.index) { + if(a.charsLeft < b.charsLeft) { + return -1; + } else if(a.charsLeft > b.charsLeft) { + return 1; + } else { + return 0; + } + } + }); + + for(i = 0, choicesCount = choicesRelevance.length; i < choicesCount; i++) { + result.push(choicesRelevance[i].baseIndex); + } + + return result; + }; + + AutocompleteEditor.prototype.getDropdownHeight = function(){ + //var firstRowHeight = this.$htContainer.handsontable('getInstance').getRowHeight(0) || 23; + var firstRowHeight = this.htEditor.getInstance().getRowHeight(0) || 23; + //var firstRowHeight = Handsontable.tmpHandsontable(this.htContainer,'getInstance').getRowHeight(0) || 23; + return this.choices.length >= 10 ? 10 * firstRowHeight : this.choices.length * firstRowHeight + 8; + //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; + + 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.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 (Array.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; + } + }; + + // TODO: Refactor this with the use of new getCell() after 0.12.1 + SelectEditor.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'; + } + } + }; + + 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); + var tdOffset = Handsontable.Dom.offset(this.TD); + var editorSection = this.checkEditorSection(); + var cssTransformOffset; + + switch(editorSection) { + case 'top': + cssTransformOffset = Handsontable.Dom.getCssTransform(this.instance.view.wt.wtScrollbars.vertical.clone.wtTable.holder.parentNode); + break; + case 'left': + cssTransformOffset = Handsontable.Dom.getCssTransform(this.instance.view.wt.wtScrollbars.horizontal.clone.wtTable.holder.parentNode); + break; + case 'corner': + cssTransformOffset = Handsontable.Dom.getCssTransform(this.instance.view.wt.wtScrollbars.corner.clone.wtTable.holder.parentNode); + break; + } + + var selectStyle = this.select.style; + + if(cssTransformOffset && cssTransformOffset != -1) { + selectStyle[cssTransformOffset[0]] = cssTransformOffset[1]; + } else { + Handsontable.Dom.resetCssTransform(this.select); + } + + selectStyle.height = height + 'px'; + selectStyle.minWidth = width + 'px'; + selectStyle.top = tdOffset.top - rootOffset.top + 'px'; + selectStyle.left = tdOffset.left - rootOffset.left + 'px'; + selectStyle.margin = '0px'; + selectStyle.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); +(function (Handsontable) { + + 'use strict'; + + var NumericEditor = Handsontable.editors.TextEditor.prototype.extend(); + + NumericEditor.prototype.beginEditing = function (initialValue) { + + var BaseEditor = Handsontable.editors.TextEditor.prototype; + + if (typeof (initialValue) === 'undefined' && this.originalValue) { + + var value = '' + this.originalValue; + + if (typeof this.cellProperties.language !== 'undefined') { + numeral.language(this.cellProperties.language); + } + + var decimalDelimiter = numeral.languageData().delimiters.decimal; + value = value.replace('.', decimalDelimiter); + + BaseEditor.beginEditing.apply(this, [value]); + } else { + BaseEditor.beginEditing.apply(this, arguments); + } + + }; + + Handsontable.editors.NumericEditor = NumericEditor; + Handsontable.editors.registerEditor('numeric', NumericEditor); + +})(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 {*} callback - Callback called with validation result + */ +Handsontable.AutocompleteValidator = function (value, callback) { + if (this.strict && this.source) { + if ( typeof this.source === 'function' ) { + this.source(value, process(value, callback)); + } else { + process(value, callback)(this.source); + } + } else { + callback(true); + } +}; + +/** + * Cell type is just a shortcut for setting bunch of cellProperties (used in getCellMeta) + */ + +Handsontable.mobileBrowser = Handsontable.helper.isMobileBrowser(); // check if viewed on a mobile device + +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.mobileBrowser ? Handsontable.editors.MobileTextEditor : Handsontable.editors.TextEditor, + renderer: Handsontable.renderers.TextRenderer +}; + +Handsontable.NumericCell = { + editor: Handsontable.editors.NumericEditor, + 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 + }, + 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.removeEventListener) { + element.removeEventListener(event, handler, false); + } else { + element.detachEvent('on' + event, handler); + } + }, + resize = function (newChar) { + var width, scrollHeight; + + if (!newChar) { + newChar = ""; + } else if (!/^[a-zA-Z \.,\\\/\|0-9]$/.test(newChar)) { + newChar = "."; + } + + if (text.textContent !== void 0) { + text.textContent = el.value + newChar; + } + else { + text.data = el.value + newChar; //IE8 + } + span.style.fontSize = Handsontable.Dom.getComputedStyle(el).fontSize; + span.style.fontFamily = Handsontable.Dom.getComputedStyle(el).fontFamily; + span.style.whiteSpace = "pre"; + + body.appendChild(span); + width = span.clientWidth + 2; + 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'; + } + scrollHeight = el.scrollHeight ? el.scrollHeight - 1 : 0; + + 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(!span.firstChild) { + span.className = "autoResize"; + span.style.display = 'inline-block'; + span.appendChild(text); + } + }, + init = function (el_, config, doObserve) { + 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.overflowY = 'hidden'; + } + + if(doObserve) { + 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, doObserve) { + init(el_, config, doObserve); + }, + unObserve: function () { + unObserve(el, 'change', resize); + unObserve(el, 'cut', delayedResize); + unObserve(el, 'paste', delayedResize); + unObserve(el, 'drop', delayedResize); + unObserve(el, 'keydown', delayedResize); + }, + resize: resize + }; + +}; + +/** + * 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 = { + /** + * Decode spreadsheet string into array + * + * @param {String} str + * @returns {Array} + */ + 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; + }, + + /** + * Encode array into valid spreadsheet string + * + * @param arr + * @returns {String} + */ + stringify: function (arr) { + var r, rLen, c, cLen, str = '', val; + + for (r = 0, rLen = arr.length; r < rLen; r += 1) { + cLen = arr[r].length; + + for (c = 0; 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)); + +/** + * 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(); +} + +/** + * Initialize CopyPaste class + */ +CopyPasteClass.prototype.init = function () { + var + style, + parent; + + this.copyCallbacks = []; + this.cutCallbacks = []; + this.pasteCallbacks = []; + this._eventManager = Handsontable.eventManager(this); + + // 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'; + this.elTextarea.onpaste = function (event) { + if('WebkitAppearance' in document.documentElement.style) { // chrome and safari + this.value = event.clipboardData.getData("Text"); + + return false; + } + }; + 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; + } + } + this.keyDownRemoveEvent = this._eventManager.addEventListener(document.documentElement, 'keydown', this.onKeyDown.bind(this), false); +}; + +/** + * Call method on every key down event + * + * @param {DOMEvent} event + */ +CopyPasteClass.prototype.onKeyDown = function (event) { + var _this = this, + isCtrlDown = false; + + // mac + if (event.metaKey) { + isCtrlDown = true; + } + // pc + else if (event.ctrlKey && navigator.userAgent.indexOf('Mac') === -1) { + isCtrlDown = true; + } + if (isCtrlDown) { + // this is needed by fragmentSelection in Handsontable. Ignore copypaste.js behavior if fragment of cell text is selected + if (document.activeElement !== this.elTextarea && (this.getSelectionText() !== '' || + ['INPUT', 'SELECT', 'TEXTAREA'].indexOf(document.activeElement.nodeName) !== -1)) { + return; + } + + this.selectNodeText(this.elTextarea); + setTimeout(function () { + _this.selectNodeText(_this.elTextarea); + }, 0); + } + + /* 67 = c + * 86 = v + * 88 = x + */ + if (isCtrlDown && (event.keyCode === 67 || event.keyCode === 86 || event.keyCode === 88)) { + // that.selectNodeText(that.elTextarea); + + // works in all browsers, incl. Opera < 12.12 + if (event.keyCode === 88) { + setTimeout(function () { + _this.triggerCut(event); + }, 0); + } + else if (event.keyCode === 86) { + setTimeout(function () { + _this.triggerPaste(event); + }, 0); + } + } +}; + +//http://jsperf.com/textara-selection +//http://stackoverflow.com/questions/1502385/how-can-i-make-this-code-work-in-ie +/** + * Select all text contains in passed node element + * + * @param {Element} el + */ +CopyPasteClass.prototype.selectNodeText = function (el) { + if (el) { + el.select(); + } +}; + +//http://stackoverflow.com/questions/5379120/get-the-highlighted-selected-text +/** + * Get selection text + * + * @returns {String} + */ +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; +}; + +/** + * Make string copyable + * + * @param {String} str + */ +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); +};*/ + +/** + * Add function callback to onCut event + * + * @param {Function} fn + */ +CopyPasteClass.prototype.onCut = function (fn) { + this.cutCallbacks.push(fn); +}; + +/** + * Add function callback to onPaste event + * + * @param {Function} fn + */ +CopyPasteClass.prototype.onPaste = function (fn) { + this.pasteCallbacks.push(fn); +}; + +/** + * Remove callback from all events + * + * @param {Function} fn + * @returns {Boolean} + */ +CopyPasteClass.prototype.removeCallback = function (fn) { + var i, len; + + for (i = 0, len = this.copyCallbacks.length; i < len; i++) { + if (this.copyCallbacks[i] === fn) { + this.copyCallbacks.splice(i, 1); + + return true; + } + } + for (i = 0, len = this.cutCallbacks.length; i < len; i++) { + if (this.cutCallbacks[i] === fn) { + this.cutCallbacks.splice(i, 1); + + return true; + } + } + for (i = 0, len = this.pasteCallbacks.length; i < len; i++) { + if (this.pasteCallbacks[i] === fn) { + this.pasteCallbacks.splice(i, 1); + + return true; + } + } + + return false; +}; + +/** + * Trigger cut event + * + * @param {DOMEvent} event + */ +CopyPasteClass.prototype.triggerCut = function (event) { + var _this = this; + + if (_this.cutCallbacks) { + setTimeout(function () { + for (var i = 0, len = _this.cutCallbacks.length; i < len; i++) { + _this.cutCallbacks[i](event); + } + }, 50); + } +}; + +/** + * Trigger paste event + * + * @param {DOMEvent} event + * @param {String} str + */ +CopyPasteClass.prototype.triggerPaste = function (event, str) { + var _this = this; + + if (_this.pasteCallbacks) { + setTimeout(function () { + var val = str || _this.elTextarea.value; + + for (var i = 0, len = _this.pasteCallbacks.length; i < len; i++) { + _this.pasteCallbacks[i](val, event); + } + }, 50); + } +}; + +/** + * Destroy instance + */ +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.keyDownRemoveEvent(); + } +}; + +/** + * Check if instance has been destroyed + * + * @returns {Boolean} + */ +CopyPasteClass.prototype.hasBeenDestroyed = function () { + return !this.refCounter; +}; + + + +// 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); + /* jshint ignore:start */ + if (found != '') { + return escapePathComponent(key) + '/' + found; + } + /* jshint ignore:end */ + } + } + } + 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 = []; + /* jshint ignore:start */ + jsonpatch.intervals; + /* jshint ignore:end */ + 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; + /* jshint ignore:start */ + 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++; + } + /* jshint ignore:end */ + + 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: [], + beforeDrawBorders: [], + beforeChange: [], + beforeChangeRender: [], + beforeRemoveCol: [], + beforeRemoveRow: [], + beforeValidate: [], + beforeGetCellMeta: [], + beforeAutofill: [], + beforeKeyDown: [], + beforeOnCellMouseDown: [], + beforeTouchScroll: [], + 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: [], + afterIsMultipleSelectionCheck: [], + afterDocumentKeyDown: [], + afterMomentumScroll: [], + + // 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() { + /* jshint ignore:start */ + this.hooks = Hooks(); + /* jshint ignore:end */ + 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 (Array.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 been added (adding the same hook twice is now silently ignored) + } + } + return this; + }; + + PluginHookClass.prototype.once = function(key, fn, instance){ + + if(Array.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.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.run = 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._runBucket(this.globalBucket, instance, key, p1, p2, p3, p4, p5, p6); + p1 = this._runBucket(this.getBucket(instance), instance, key, p1, p2, p3, p4, p5, p6); + + return p1; + }; + + PluginHookClass.prototype._runBucket = function (bucket, instance, key, p1, p2, p3, p4, p5, p6) { + var handlers = bucket[key], + res, i, len; + + // performance considerations - http://jsperf.com/call-vs-apply-for-a-plugin-architecture + if (handlers) { + for (i = 0, len = handlers.length; i < len; 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); + } + } + } + } + + 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.className + ' htAutoColumnSize'; + tmp.table.className = instance.table.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.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.className + ' hidden'; +// 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; + + var eventManager = Handsontable.eventManager(instance); + eventManager.addEventListener(instance.rootElement, 'click', 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(typeof a[1] == "string") { + a[1] = a[1].toLowerCase(); + } + if(typeof b[1] == "string") { + b[1] = b[1].toLowerCase(); + } + + if (a[1] === b[1]) { + return 0; + } + if (a[1] === null || a[1] === "") { + return 1; + } + if (b[1] === null || b[1] === "") { + 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 && col >= 0) { + 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) { + /* jshint ignore:start */ + 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); + + } + + function align(range, type, alignment) { + /* jshint ignore:start */ + 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); + } + } + } + + this.render(); + + /* jshint ignore:end */ + } + + function ContextMenu(instance, customOptions) { + this.instance = instance; + var contextMenu = this; + contextMenu.menus = []; + contextMenu.htMenus = {}; + contextMenu.triggerRows = []; + + contextMenu.eventManager = Handsontable.eventManager(contextMenu); + + + this.enabled = true; + + this.instance.addHook('afterDestroy', function () { + contextMenu.destroy(); + }); + + this.defaultOptions = { + items: [ + { + key: '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.countRows() - 1, selected[1]], + columnSelected = entireColumnSelection.join(',') == selected.join(','); + + return selected[0] < 0 || this.countRows() >= this.getSettings().maxRows || columnSelected; + } + }, + { + key: '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.countRows() - 1, selected[1]], + columnSelected = entireColumnSelection.join(',') == selected.join(','); + + return this.getSelected()[0] < 0 || this.countRows() >= this.getSettings().maxRows || columnSelected; + } + }, + ContextMenu.SEPARATOR, + { + key: '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.countCols() - 1], + rowSelected = entireRowSelection.join(',') == selected.join(','); + + return this.getSelected()[1] < 0 || this.countCols() >= this.getSettings().maxCols || rowSelected; + } + }, + { + key: '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.countCols() - 1], + rowSelected = entireRowSelection.join(',') == selected.join(','); + + return selected[1] < 0 || this.countCols() >= this.getSettings().maxCols || rowSelected; + } + }, + ContextMenu.SEPARATOR, + { + key: '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.countRows() - 1, selected[1]], + columnSelected = entireColumnSelection.join(',') == selected.join(','); + return (selected[0] < 0 || columnSelected); + } + }, + { + key: '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.countCols() - 1], + rowSelected = entireRowSelection.join(',') == selected.join(','); + return (selected[1] < 0 || rowSelected); + } + }, + ContextMenu.SEPARATOR, + { + key: 'undo', + name: 'Undo', + callback: function () { + this.undo(); + }, + disabled: function () { + return this.undoRedo && !this.undoRedo.isUndoAvailable(); + } + }, + { + key: 'redo', + name: 'Redo', + callback: function () { + this.redo(); + }, + disabled: function () { + return this.undoRedo && !this.undoRedo.isRedoAvailable(); + } + }, + ContextMenu.SEPARATOR, + { + key: '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(); + } + }, + ContextMenu.SEPARATOR, + { + key: 'alignment', + name: 'Alignment', + submenu: { + items: [ + { + 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 + }, + { + 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 + }, + { + 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 + }, + { + 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 + }, + ContextMenu.SEPARATOR, + { + 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 + }, + { + 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 + }, + { + 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.options); + + this.bindMouseEvents(); + + this.markSelected = function (label) { + return "" + String.fromCharCode(10003) + "" + label; // workaround for https://github.com/handsontable/handsontable/issues/1946 + }; + + 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; + }; + + if(!this.instance.getSettings().allowInsertRow) { + var rowAboveIndex = findIndexByKey(this.defaultOptions.items, 'row_above'); + this.defaultOptions.items.splice(rowAboveIndex,1); + var rowBelowIndex = findIndexByKey(this.defaultOptions.items, 'row_above'); + this.defaultOptions.items.splice(rowBelowIndex,1); + this.defaultOptions.items.splice(rowBelowIndex,1); // FOR SEPARATOR + + } + + if(!this.instance.getSettings().allowInsertColumn) { + var colLeftIndex = findIndexByKey(this.defaultOptions.items, 'col_left'); + this.defaultOptions.items.splice(colLeftIndex,1); + var colRightIndex = findIndexByKey(this.defaultOptions.items, 'col_right'); + this.defaultOptions.items.splice(colRightIndex,1); + this.defaultOptions.items.splice(colRightIndex,1); // FOR SEPARATOR + + } + + var removeRow = false; + var removeCol = false; + var removeRowIndex, removeColumnIndex; + + if(!this.instance.getSettings().allowRemoveRow) { + removeRowIndex = findIndexByKey(this.defaultOptions.items, 'remove_row'); + this.defaultOptions.items.splice(removeRowIndex,1); + removeRow = true; + } + + if(!this.instance.getSettings().allowRemoveColumn) { + removeColumnIndex = findIndexByKey(this.defaultOptions.items, 'remove_col'); + this.defaultOptions.items.splice(removeColumnIndex,1); + removeCol = true; + } + + if (removeRow && removeCol) { + this.defaultOptions.items.splice(removeColumnIndex,1); // SEPARATOR + } + + 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); + + } + + /*** + * Create DOM instance of contextMenu + * @param menuName + * @param row + * @return {*} + */ + 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 = document.querySelector('.htContextMenu.' + menuName); + } else { + menu = document.querySelector('.htContextMenu'); + } + + + 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 () { + /* jshint ignore:start */ + function contextMenuOpenListener(event) { + var settings = this.instance.getSettings(); + + this.closeAll(); + + event.preventDefault(); + Handsontable.helper.stopPropagation(event); + + 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(settings.contextMenu); + + this.show(menu, items); + + this.setMenuPosition(event, menu); + + this.eventManager.addEventListener(document.documentElement, 'mousedown', Handsontable.helper.proxy(ContextMenu.prototype.closeAll, this)); + } + /* jshint ignore:end */ + var eventManager = Handsontable.eventManager(this.instance); + + eventManager.addEventListener(this.instance.rootElement, 'contextmenu', Handsontable.helper.proxy(contextMenuOpenListener, this)); + }; + + ContextMenu.prototype.bindTableEvents = function () { + this._afterScrollCallback = function () {}; + 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, hot) { + var contextMenu = this; + + 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(); + } + }; + + ContextMenu.prototype.unbindMouseEvents = function () { + this.eventManager.clear(); + var eventManager = Handsontable.eventManager(this.instance); + eventManager.removeEventListener(this.instance.rootElement, 'contextmenu'); + }; + + ContextMenu.prototype.show = function (menu, items) { + var that = this; + + menu.removeAttribute('style'); + menu.style.display = 'block'; + + var settings = { + data: items, + colHeaders: false, + colWidths: [200], + readOnly: true, + copyPaste: false, + columns: [ + { + data: 'name', + renderer: Handsontable.helper.proxy(this.renderer, this) + } + ], + renderAllRows: true, + beforeKeyDown: function (event) { + that.onBeforeKeyDown(event, htContextMenu); + }, + afterOnCellMouseOver: function (event, coords, TD) { + that.onCellMouseOver(event, coords, TD, htContextMenu); + } + }; + + var htContextMenu = new Handsontable(menu, settings); + + + this.eventManager.removeEventListener(menu, 'mousedown'); + this.eventManager.addEventListener(menu,'mousedown', function (event) { + that.performAction(event, htContextMenu); + }); + + this.bindTableEvents(); + htContextMenu.listen(); + + this.htMenus[htContextMenu.guid] = htContextMenu; + }; + + ContextMenu.prototype.close = function (menu) { + this.hide(menu); + this.eventManager.clear(); + 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); + } + + }; + + ContextMenu.prototype.hide = function (menu) { + menu.style.display = 'none'; + var instance =this.htMenus[menu.id]; + + instance.destroy(); + delete this.htMenus[menu.id]; + }; + + 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'); + + this.eventManager.addEventListener(wrapper, 'mouseenter', function () { + instance.deselectCell(); + }); + + } else { + if (isSubMenu(item)) { + Handsontable.Dom.addClass(TD, 'htSubmenu'); + + + this.eventManager.addEventListener(wrapper, 'mouseenter', function () { + instance.selectCell(row, col); + }); + + } else { + Handsontable.Dom.removeClass(TD, 'htSubmenu'); + Handsontable.Dom.removeClass(TD, 'htDisabled'); + + this.eventManager.addEventListener(wrapper, 'mouseenter', function () { + instance.selectCell(row, col); + }); + } + } + + + function isSubMenu(item) { + return item.hasOwnProperty('submenu'); + } + + function itemIsSeparator(item) { + return new RegExp(ContextMenu.SEPARATOR.name, '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, hot) { + var menusLength = this.menus.length; + + if (menusLength > 0) { + var lastMenu = this.menus[menusLength - 1]; + if (lastMenu.id != hot.guid) { + 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, instance) { + + Handsontable.Dom.enableImmediatePropagation(event); + var contextMenu = this; + + 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, instance); + } + 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 (instance.rootElement.className.indexOf('htContextSubMenu_') != -1) { + contextMenu.closeLastOpenedSubMenu(); + var index = contextMenu.menus.length; + + if (index > 0) { + var menu = contextMenu.menus[index - 1]; + + var triggerRow = contextMenu.triggerRows.pop(); + instance = this.htMenus[menu.id]; + 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(); + var subMenuInstance = contextMenu.show(subMenu, items); + + contextMenu.setSubMenuPosition(coords, subMenu); + subMenuInstance.selectCell(0, 0); + } + }; + + function findByKey(items, key) { + for (var i = 0, ilen = items.length; i < ilen; i++) { + if (items[i].key === key) { + return items[i]; + } + } + } + + function findIndexByKey(items, key) { + for (var i = 0, ilen = items.length; i < ilen; i++) { + if (items[i].key === key) { + return i; + } + } + } + + ContextMenu.prototype.getItems = function (items) { + var menu, item; + + function ContextMenuItem(rawItem) { + if (typeof rawItem == 'string') { + this.name = rawItem; + } else { + Handsontable.helper.extend(this, rawItem); + } + } + + ContextMenuItem.prototype = items; + + if (items && items.items) { + items = items.items; + } + + if (items === true) { + items = this.defaultOptions.items; + } + + if (1 == 1) { + menu = []; + for (var key in items) { + if (items.hasOwnProperty(key)) { + if (typeof items[key] === 'string') { + item = findByKey(this.defaultOptions.items, items[key]); + } + else { + item = findByKey(this.defaultOptions.items, key); + } + if (!item) { + item = items[key]; + } + item = new ContextMenuItem(item); + if (typeof items[key] === 'object') { + Handsontable.helper.extend(item, items[key]); + } + if (!item.key) { + item.key = key; + } + menu.push(item); + } + } + } + + return menu; + }; + + 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, document.body.clientWidth)) { + 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, document.body.clientWidth)) { + this.positionMenuOnRightOfCursor(cursor, menu, true); + } else { + this.positionMenuOnLeftOfCursor(cursor, menu, true); + } + }; + + ContextMenu.prototype.setMenuPosition = function (event, menu) { + // for ie8 + // http://msdn.microsoft.com/en-us/library/ie/ff974655(v=vs.85).aspx + var scrollTop = Handsontable.Dom.getWindowScrollTop(); + var scrollLeft = Handsontable.Dom.getWindowScrollLeft(); + var cursorY = event.pageY || (event.clientY + scrollTop); + var cursorX = event.pageX || (event.clientX + scrollLeft); + + 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, document.body.clientHeight)) { + this.positionMenuBelowCursor(cursor, menu); + } else { + if (this.menuFitsAboveCursor(cursor, menu)) { + this.positionMenuAboveCursor(cursor, menu); + } else { + this.positionMenuBelowCursor(cursor, menu); + } + } + + if (this.menuFitsOnRightOfCursor(cursor, menu, document.body.clientWidth)) { + 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, viewportHeight) { + return cursor.topRelative + menu.offsetHeight <= viewportHeight; + }; + + ContextMenu.prototype.menuFitsOnRightOfCursor = function (cursor, menu, viewportHeight) { + return cursor.leftRelative + menu.offsetWidth <= viewportHeight; + }; + + 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 = 1 + cursor.left + cursor.cellWidth + 'px'; + } else { + menu.style.left = 1 + 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.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 = document.querySelectorAll('.handsontable'); + var menuEnabled = false; + + for (var i = 0, len = hotContainers.length; i < len; i++) { + var instance = this.htMenus[hotContainers[i].id]; + if (instance && instance.getSettings().contextMenu) { + menuEnabled = true; + break; + } + } + + return menuEnabled; + }; + + ContextMenu.prototype.removeMenu = function (menu) { + if (menu.parentNode) { + this.menu.parentNode.removeChild(menu); + } + }; + + ContextMenu.SEPARATOR = {name: "---------"}; + + function updateHeight() { + /* jshint ignore:start */ + if (this.rootElement.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.name) { + realSeparatorHeight += 2; + } else { + realEntrySize += 26; + } + } + + this.view.wt.wtScrollbars.vertical.fixedContainer.style.height = realEntrySize + realSeparatorHeight + "px"; + /* jshint ignore:end */ + } + + function init() { + /* jshint ignore:start */ + var instance = this; + /* jshint ignore:end */ + 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(); + } 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); + + Handsontable.PluginHooks.register('afterContextMenuDefaultOptions'); + + Handsontable.ContextMenu = ContextMenu; + +})(Handsontable); + +function Comments(instance) { + + var eventManager = Handsontable.eventManager(instance), + 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) { + eventManager.removeEventListener(document, 'mouseover'); + if (!(event.target.className == 'htCommentTextArea' || event.target.innerHTML.indexOf('Comment') != -1)) { + var value = document.querySelector('.htCommentTextArea').value; + if (value.trim().length > 1) { + saveComment(range, value, instance); + } + unBindMouseEvent(); + hideCommentTextArea(); + } + } + + eventManager.addEventListener(document, 'mousedown',Handsontable.helper.proxy(commentsListener)); + }, + unBindMouseEvent = function () { + eventManager.removeEventListener(document, 'mousedown'); + eventManager.addEventListener(document, 'mousedown', 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.querySelector('.htComments'); + + 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.querySelector('.htCommentTextArea').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 () { + eventManager.addEventListener(document, 'mouseover', 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.push(Handsontable.ContextMenu.SEPARATOR); + + defaultOptions.items.push({ + key: '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.push({ + key: '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); + + +/** + * 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') + , eventManager = Handsontable.eventManager(this); + + 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.appendChild(handle); + } + } + + function refreshHandlePosition(TH, delta) { + var box = TH.getBoundingClientRect(); + var handleWidth = 6; + if (delta > 0) { + handle.style.left = (box.left + box.width - handleWidth) + 'px'; + } + else { + 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.appendChild(guide); + } + + function refreshGuidePosition(diff) { + guide.style.left = startOffset + diff + 'px'; + } + + function hideHandleAndGuide() { + Handsontable.Dom.removeClass(handle, 'active'); + Handsontable.Dom.removeClass(guide, 'active'); + } + + var checkColumnHeader = function (element) { + if (element.tagName != 'BODY') { + if (element.parentNode.tagName == 'THEAD') { + return true; + } else { + element = element.parentNode; + return checkColumnHeader(element); + } + } + return false; + }; + + var getTHFromTargetElement = function (element) { + if (element.tagName != 'TABLE') { + if (element.tagName == 'TH') { + return element; + } else { + return getTHFromTargetElement(element.parentNode); + } + } + return null; + }; + + var bindEvents = function () { + + var instance = this; + var pressed; + + eventManager.addEventListener(instance.rootElement,'mouseover',function (e) { + if (checkColumnHeader(e.target)){ + var th = getTHFromTargetElement(e.target); + if (th) { + if (pressed) { + var col = instance.view.wt.wtTable.getCoords(th).col; + if(col >= 0) { //not TH above row header + endCol = col; + refreshHandlePosition(e.target, endCol - startCol); + } + } + else { + setupHandlePosition.call(instance, th); + } + } + } + }); + + eventManager.addEventListener(instance.rootElement,'mousedown', function (e) { + if (Handsontable.Dom.hasClass(e.target, 'manualColumnMover')){ + startX = Handsontable.helper.pageX(e); + setupGuidePosition.call(instance); + pressed = instance; + + startCol = currentCol; + endCol = currentCol; + } + }); + + eventManager.addEventListener(window,'mousemove',function (e) { + if (pressed) { + refreshGuidePosition(Handsontable.helper.pageX(e) - startX); + } + }); + + + eventManager.addEventListener(window,'mouseup',function (e) { + if (pressed) { + hideHandleAndGuide(); + pressed = false; + + 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(){ + eventManager.clear(); + }; + + 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 (Array.isArray(initialManualColumnPositions)) { + this.manualColumnPositions = initialManualColumnPositions; + } else { + this.manualColumnPositions = []; + } + + if (source == 'afterInit') { + + // update plugin usages count for manualColumnPositions + if (typeof instance.manualColumnPositionsPluginUsages != 'undefined') { + instance.manualColumnPositionsPluginUsages.push('manualColumnMove'); + } else { + instance.manualColumnPositionsPluginUsages = ['manualColumnMove']; + } + + bindEvents.call(this); + if (this.manualColumnPositions.length > 0) { + this.forceFullRender = true; + this.render(); + } + } + + } else { + var pluginUsagesIndex = instance.manualColumnPositionsPluginUsages ? instance.manualColumnPositionsPluginUsages.indexOf('manualColumnMove') : -1; + if (pluginUsagesIndex > -1) { + unbindEvents.call(this); + this.manualColumnPositions = []; + instance.manualColumnPositionsPluginUsages[pluginUsagesIndex] = void 0; + } + } + }; + + 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; + }; + + // need to reconstruct manualcolpositions after removing columns + this.afterRemoveCol = function (index, amount) { + if (!this.getSettings().manualColumnMove) { + return; + } + + var rmindx, + colpos = this.manualColumnPositions; + + // We have removed columns, we also need to remove the indicies from manual column array + rmindx = colpos.splice(index, amount); + + // We need to remap manualColPositions so it remains constant linear from 0->ncols + colpos = colpos.map(function (colpos) { + var i, newpos = colpos; + + for (i = 0; i < rmindx.length; i++) { + if (colpos > rmindx[i]) { + newpos--; + } + } + + return newpos; + }); + + this.manualColumnPositions = colpos; + }; + + // need to reconstruct manualcolpositions after adding columns + this.afterCreateCol = function (index, amount) { + if (!this.getSettings().manualColumnMove) { + return; + } + + var colpos = this.manualColumnPositions; + if (!colpos.length) { + return; + } + + var addindx = []; + for (var i = 0; i < amount; i++) { + addindx.push(index + i); + } + + if (index >= colpos.length) { + colpos.concat(addindx); + } + else { + // We need to remap manualColPositions so it remains constant linear from 0->ncols + colpos = colpos.map(function (colpos) { + return (colpos >= index) ? (colpos + amount) : colpos; + }); + + // We have added columns, we also need to add new indicies to manualcolumn position array + colpos.splice.apply(colpos, [index, 0].concat(addindx)); + } + + this.manualColumnPositions = colpos; + }; +} +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.add('afterRemoveCol', htManualColumnMove.afterRemoveCol); +Handsontable.hooks.add('afterCreateCol', htManualColumnMove.afterCreateCol); +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') + , eventManager = Handsontable.eventManager(this); + + + 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.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.appendChild(guide); + } + + function refreshGuidePosition() { + guide.style.left = handle.style.left; + } + + function hideHandleAndGuide() { + Handsontable.Dom.removeClass(handle, 'active'); + Handsontable.Dom.removeClass(guide, 'active'); + } + + var checkColumnHeader = function (element) { + if (element.tagName != 'BODY') { + if (element.parentNode.tagName == 'THEAD') { + return true; + } else { + element = element.parentNode; + return checkColumnHeader(element); + } + } + return false; + }; + + var getTHFromTargetElement = function (element) { + if (element.tagName != 'TABLE') { + if (element.tagName == 'TH') { + return element; + } else { + return getTHFromTargetElement(element.parentNode); + } + } + return null; + }; + + var bindEvents = function () { + var instance = this; + var pressed; + var dblclick = 0; + var autoresizeTimeout = null; + + eventManager.addEventListener(instance.rootElement, 'mouseover', function (e) { + if (checkColumnHeader(e.target)) { + var th = getTHFromTargetElement(e.target); + if (th) { + if (!pressed) { + setupHandlePosition.call(instance, th); + } + } + } + }); + + eventManager.addEventListener(instance.rootElement, 'mousedown', function (e) { + if (Handsontable.Dom.hasClass(e.target, 'manualColumnResizer')) { + 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 = Handsontable.helper.pageX(e); + newSize = startWidth; + } + }); + + eventManager.addEventListener(window, 'mousemove', function (e) { + if (pressed) { + currentWidth = startWidth + (Handsontable.helper.pageX(e) - startX); + newSize = setManualSize(currentCol, currentWidth); //save col width + refreshHandlePosition(); + refreshGuidePosition(); + } + }); + + eventManager.addEventListener(window, 'mouseup', 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 () { + eventManager.clear(); + }; + + 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); + + // update plugin usages count for manualColumnPositions + if (typeof instance.manualColumnWidthsPluginUsages != 'undefined') { + instance.manualColumnWidthsPluginUsages.push('manualColumnResize'); + } else { + instance.manualColumnWidthsPluginUsages = ['manualColumnResize']; + } + + if (typeof loadedManualColumnWidths != 'undefined') { + this.manualColumnWidths = loadedManualColumnWidths; + } else if (Array.isArray(initialColumnWidths)) { + this.manualColumnWidths = initialColumnWidths; + } else { + this.manualColumnWidths = []; + } + + if (source == 'afterInit') { + bindEvents.call(this); + if (this.manualColumnWidths.length > 0) { + this.forceFullRender = true; + this.render(); + } + } + } + else { + var pluginUsagesIndex = instance.manualColumnWidthsPluginUsages ? instance.manualColumnWidthsPluginUsages.indexOf('manualColumnResize') : -1; + if (pluginUsagesIndex > -1) { + 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.run(instance, 'modifyCol', col); + instance.manualColumnWidths[col] = width; + + return width; + }; + + this.modifyColWidth = function (width, col) { + col = this.runHooks('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') + , eventManager = Handsontable.eventManager(this); + + 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.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.appendChild(guide); + } + + function refreshGuidePosition() { + guide.style.top = handle.style.top; + } + + function hideHandleAndGuide() { + Handsontable.Dom.removeClass(handle, 'active'); + Handsontable.Dom.removeClass(guide, 'active'); + } + + var checkRowHeader = function (element) { + if (element.tagName != 'BODY') { + if (element.parentNode.tagName == 'TBODY') { + return true; + } else { + element = element.parentNode; + return checkRowHeader(element); + } + } + return false; + }; + + var getTHFromTargetElement = function (element) { + if (element.tagName != 'TABLE') { + if (element.tagName == 'TH') { + return element; + } else { + return getTHFromTargetElement(element.parentNode); + } + } + return null; + }; + + var bindEvents = function () { + var instance = this; + var pressed; + var dblclick = 0; + var autoresizeTimeout = null; + + eventManager.addEventListener(instance.rootElement, 'mouseover', function (e) { + if (checkRowHeader(e.target)) { + var th = getTHFromTargetElement(e.target); + if (th) { + if (!pressed) { + setupHandlePosition.call(instance, th); + } + } + } + }); + + eventManager.addEventListener(instance.rootElement, 'mousedown', function (e) { + if (Handsontable.Dom.hasClass(e.target, 'manualRowResizer')) { + 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 = Handsontable.helper.pageY(e); + newSize = startHeight; + } + }); + + eventManager.addEventListener(window, 'mousemove', function (e) { + if (pressed) { + currentHeight = startHeight + (Handsontable.helper.pageY(e) - startY); + newSize = setManualSize(currentRow, currentHeight); + refreshHandlePosition(); + refreshGuidePosition(); + } + }); + + eventManager.addEventListener(window, 'mouseup', function (e) { + 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 () { + eventManager.clear(); + }; + + 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); + + // update plugin usages count for manualColumnPositions + if (typeof instance.manualRowHeightsPluginUsages != 'undefined') { + instance.manualRowHeightsPluginUsages.push('manualRowResize'); + } else { + instance.manualRowHeightsPluginUsages = ['manualRowResize']; + } + + if (typeof loadedManualRowHeights != 'undefined') { + this.manualRowHeights = loadedManualRowHeights; + } else if (Array.isArray(initialRowHeights)) { + 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 { + var pluginUsagesIndex = instance.manualRowHeightsPluginUsages ? instance.manualRowHeightsPluginUsages.indexOf('manualRowResize') : -1; + if (pluginUsagesIndex > -1) { + unbindEvents.call(this); + this.manualRowHeights = []; + instance.manualRowHeightsPluginUsages[pluginUsagesIndex] = void 0; + } + } + }; + + var setManualSize = function (row, height) { + row = Handsontable.hooks.run(instance, 'modifyRow', row); + instance.manualRowHeights[row] = height; + + return height; + }; + + this.modifyRowHeight = function (height, row) { + if (this.getSettings().manualRowResize) { + row = this.runHooks('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.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(Array.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 = Handsontable.helper.deepClone(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 = Handsontable.helper.deepClone(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 { + dragToScroll.setBoundaries(scrollHandler.getBoundingClientRect()); + } + + dragToScroll.setCallback(function (scrollX, scrollY) { + if (scrollX < 0) { + scrollHandler.scrollLeft -= 50; + } + else if (scrollX > 0) { + scrollHandler.scrollLeft += 50; + } + + if (scrollY < 0) { + scrollHandler.scrollTop -= 20; + } + else if (scrollY > 0) { + scrollHandler.scrollTop += 20; + } + }); + + instance.dragToScrollListening = true; + }; + + Handsontable.hooks.add('afterInit', function () { + var instance = this; + var eventManager = Handsontable.eventManager(this); + + eventManager.addEventListener(document,'mouseup', function () { + instance.dragToScrollListening = false; + }); + + eventManager.addEventListener(document,'mousemove', function (event) { + if (instance.dragToScrollListening) { + dragToScroll.check(event.clientX, event.clientY); + } + }); + }); + + Handsontable.hooks.add('afterDestroy', function () { + var eventManager = Handsontable.eventManager(this); + eventManager.clear(); + }); + + 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) { + var _this = this; + + this.copyPasteInstance = CopyPaste.getInstance(); + this.copyPasteInstance.onCut(onCut); + this.copyPasteInstance.onPaste(onPaste); + + instance.addHook('beforeKeyDown', onBeforeKeyDown); + + function onCut() { + if (!instance.isListening()) { + return; + } + instance.selection.empty(); + } + + function onPaste(str) { + var + input, + inputArray, + selected, + coordsFrom, + coordsTo, + cellRange, + topLeftCorner, + bottomRightCorner, + areaStart, + areaEnd; + + if (!instance.isListening() || !instance.selection.isSelected()) { + return; + } + input = str; + 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) { + var ctrlDown; + + if (instance.getSelected()) { + if (Handsontable.helper.isCtrlKey(event.keyCode)) { + // when CTRL is pressed, prepare selectable text in textarea + // http://stackoverflow.com/questions/3902635/how-does-one-capture-a-macs-command-key-via-javascript + _this.setCopyableText(); + event.stopImmediatePropagation(); + + return; + } + // catch CTRL but not right ALT (which in some systems triggers ALT+CTRL) + ctrlDown = (event.ctrlKey || event.metaKey) && !event.altKey; + + if (event.keyCode == Handsontable.helper.keyCode.A && ctrlDown) { + instance._registerTimeout(setTimeout(Handsontable.helper.proxy(_this.setCopyableText, _this), 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, + 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() { + /* jshint ignore:start */ + var instance = this; + /* jshint ignore:end */ + + 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 (Array.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.removeAttribute('rowspan'); + TD.removeAttribute('colspan'); + + TD.style.display = "none"; + } + } + else { + TD.removeAttribute('rowspan'); + TD.removeAttribute('colspan'); + } +}; + +MergeCells.prototype.modifyTransform = function (hook, currentSelectedRange, delta) { + var sameRowspan = function (merged, coords) { + if (coords.row >= merged.row && coords.row <= (merged.row + merged.rowspan - 1)) { + return true; + } + return false; + } + , sameColspan = function (merged, coords) { + if (coords.col >= merged.col && coords.col <= (merged.col + merged.colspan - 1)) { + return true; + } + return false; + } + , getNextPosition = function (newDelta) { + return new WalkontableCellCoords(currentSelectedRange.to.row + newDelta.row, currentSelectedRange.to.col + newDelta.col); + }; + + var newDelta = { + row: delta.row, + col: delta.col + }; + + + if (hook == 'modifyTransformStart') { + + if (!this.lastDesiredCoords) { + this.lastDesiredCoords = new WalkontableCellCoords(null, null); + } + var currentPosition = new WalkontableCellCoords(currentSelectedRange.highlight.row, currentSelectedRange.highlight.col) + , mergedParent = this.mergedCellInfoCollection.getInfo(currentPosition.row, currentPosition.col)// if current position's parent is a merged range, returns it + , currentRangeContainsMerge; // if current range contains a merged range + + for (var i = 0, mergesLength = this.mergedCellInfoCollection.length; i < mergesLength; i++) { + var range = this.mergedCellInfoCollection[i]; + range = new WalkontableCellCoords(range.row + range.rowspan - 1, range.col + range.colspan - 1); + if (currentSelectedRange.includes(range)) { + currentRangeContainsMerge = true; + break; + } + } + + if (mergedParent) { // only merge selected + var mergeTopLeft = new WalkontableCellCoords(mergedParent.row, mergedParent.col) + , mergeBottomRight = new WalkontableCellCoords(mergedParent.row + mergedParent.rowspan - 1, mergedParent.col + mergedParent.colspan - 1) + , mergeRange = new WalkontableCellRange(mergeTopLeft, mergeTopLeft, mergeBottomRight); + + if (!mergeRange.includes(this.lastDesiredCoords)) { + this.lastDesiredCoords = new WalkontableCellCoords(null, null); // reset outdated version of lastDesiredCoords + } + + newDelta.row = this.lastDesiredCoords.row ? this.lastDesiredCoords.row - currentPosition.row : newDelta.row; + newDelta.col = this.lastDesiredCoords.col ? this.lastDesiredCoords.col - currentPosition.col : newDelta.col; + + if (delta.row > 0) { // moving down + newDelta.row = mergedParent.row + mergedParent.rowspan - 1 - currentPosition.row + delta.row; + } else if (delta.row < 0) { //moving up + newDelta.row = currentPosition.row - mergedParent.row + delta.row; + } + if (delta.col > 0) { // moving right + newDelta.col = mergedParent.col + mergedParent.colspan - 1 - currentPosition.col + delta.col; + } else if (delta.col < 0) { // moving left + newDelta.col = currentPosition.col - mergedParent.col + delta.col; + } + } + + var nextPosition = new WalkontableCellCoords(currentSelectedRange.highlight.row + newDelta.row, currentSelectedRange.highlight.col + newDelta.col) + , nextParentIsMerged = this.mergedCellInfoCollection.getInfo(nextPosition.row, nextPosition.col); + + if (nextParentIsMerged) { // skipping the invisible cells in the merge range + this.lastDesiredCoords = nextPosition; + newDelta = { + row: nextParentIsMerged.row - currentPosition.row, + col: nextParentIsMerged.col - currentPosition.col + }; + } + } else if (hook == 'modifyTransformEnd') { + for (var i = 0, mergesLength = this.mergedCellInfoCollection.length; i < mergesLength; i++) { + var currentMerge = this.mergedCellInfoCollection[i] + , mergeTopLeft = new WalkontableCellCoords(currentMerge.row, currentMerge.col) + , mergeBottomRight = new WalkontableCellCoords(currentMerge.row + currentMerge.rowspan - 1, currentMerge.col + currentMerge.colspan - 1) + , mergedRange = new WalkontableCellRange(mergeTopLeft, mergeTopLeft, mergeBottomRight) + , sharedBorders = currentSelectedRange.getBordersSharedWith(mergedRange); + + if (mergedRange.isEqual(currentSelectedRange)) { // only the merged range is selected + currentSelectedRange.setDirection("NW-SE"); + } + else if (sharedBorders.length > 0) { + var mergeHighlighted = (currentSelectedRange.highlight.isEqual(mergedRange.from)); + + if (sharedBorders.indexOf('top') > -1) { // if range shares a border with the merged section, change range direction accordingly + if (currentSelectedRange.to.isSouthEastOf(mergedRange.from) && mergeHighlighted) { + currentSelectedRange.setDirection("NW-SE"); + } else if (currentSelectedRange.to.isSouthWestOf(mergedRange.from) && mergeHighlighted) { + currentSelectedRange.setDirection("NE-SW"); + } + } else if (sharedBorders.indexOf('bottom') > -1) { + if (currentSelectedRange.to.isNorthEastOf(mergedRange.from) && mergeHighlighted) { + currentSelectedRange.setDirection("SW-NE"); + } else if (currentSelectedRange.to.isNorthWestOf(mergedRange.from) && mergeHighlighted) { + currentSelectedRange.setDirection("SE-NW"); + } + } + } + + var nextPosition = getNextPosition(newDelta) + , withinRowspan = sameRowspan(currentMerge, nextPosition) + , withinColspan = sameColspan(currentMerge, nextPosition); + + if (currentSelectedRange.includesRange(mergedRange) && (mergedRange.includes(nextPosition) || withinRowspan || withinColspan)) { // if next step overlaps a merged range, jump past it + if (withinRowspan) { + if (newDelta.row < 0) { + newDelta.row -= currentMerge.rowspan - 1; + } else if (newDelta.row > 0) { + newDelta.row += currentMerge.rowspan - 1; + } + } + if (withinColspan) { + if (newDelta.col < 0) { + newDelta.col -= currentMerge.colspan - 1; + } else if (newDelta.col > 0) { + newDelta.col += currentMerge.colspan - 1; + } + } + } + } + } + + if (newDelta.row !== 0) { + delta.row = newDelta.row; + } + if (newDelta.col !== 0) { + delta.col = newDelta.col; + } +}; + +if (typeof Handsontable == 'undefined') { + throw new Error('Handsontable is not defined'); +} + +var beforeInit = function () { + var instance = this; + var mergeCellsSetting = instance.getSettings().mergeCells; + + if (mergeCellsSetting) { + if (!instance.mergeCells) { + instance.mergeCells = new MergeCells(mergeCellsSetting); + } + } +}; + +var afterInit = function () { + var instance = this; + if (instance.mergeCells) { + /** + * Monkey patch WalkontableTable.prototype.getCell to return TD for merged cell parent if asked for TD of a cell that is + * invisible due to the merge. This is not the cleanest solution but there is a test case for it (merged cells scroll) so feel free to refactor it! + */ + instance.view.wt.wtTable.getCell = function (coords) { + if (instance.getSettings().mergeCells) { + var mergeParent = instance.mergeCells.mergedCellInfoCollection.getInfo(coords.row, coords.col); + if (mergeParent) { + coords = mergeParent; + } + } + return WalkontableTable.prototype.getCell.call(this, coords); + }; + } +}; + +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.push(Handsontable.ContextMenu.SEPARATOR); + + defaultOptions.items.push({ + key: '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; + + var rangeExpanded = false; + do { + rangeExpanded = false; + + 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)) { + coords.row = selRange.to.row; + coords.col = selRange.to.col; + + rangeExpanded = true; + } + } + } while (rangeExpanded); + + } +}; + +/** + * Returns correct coordinates for merged start / end cells in selection for area borders + * @param corners + * @param className + */ +var beforeDrawAreaBorders = function (corners, className) { + if (className && className == 'area') { + var mergeCellsSetting = this.getSettings().mergeCells; + if (mergeCellsSetting) { + var selRange = this.getSelectedRange(); + var startRange = new WalkontableCellRange(selRange.from, selRange.from, selRange.from); + var stopRange = new WalkontableCellRange(selRange.to, selRange.to, selRange.to); + + 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 (startRange.expandByRange(mergedCellRange)) { + corners[0] = startRange.from.row; + corners[1] = startRange.from.col; + } + + if (stopRange.expandByRange(mergedCellRange)) { + corners[2] = stopRange.from.row; + corners[3] = stopRange.from.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; + } + } +}; + +var afterViewportRowCalculatorOverride = function (calc) { + var mergeCellsSetting = this.getSettings().mergeCells; + if (mergeCellsSetting) { + var colCount = this.countCols(); + var mergeParent; + for (var c = 0; c < colCount; c++) { + mergeParent = this.mergeCells.mergedCellInfoCollection.getInfo(calc.startRow, c); + if (mergeParent) { + if (mergeParent.row < calc.startRow) { + calc.startRow = mergeParent.row; + return afterViewportRowCalculatorOverride.call(this, calc); //recursively search upwards + } + } + mergeParent = this.mergeCells.mergedCellInfoCollection.getInfo(calc.endRow, c); + if (mergeParent) { + var mergeEnd = mergeParent.row + mergeParent.rowspan - 1; + if (mergeEnd > calc.endRow) { + calc.endRow = mergeEnd; + return afterViewportRowCalculatorOverride.call(this, calc); //recursively search upwards + } + } + } + } +}; + +var afterViewportColumnCalculatorOverride = function (calc) { + var mergeCellsSetting = this.getSettings().mergeCells; + if (mergeCellsSetting) { + var rowCount = this.countRows(); + var mergeParent; + for (var r = 0; r < rowCount; r++) { + mergeParent = this.mergeCells.mergedCellInfoCollection.getInfo(r, calc.startColumn); + + if (mergeParent) { + if (mergeParent.col < calc.startColumn) { + calc.startColumn = mergeParent.col; + return afterViewportColumnCalculatorOverride.call(this, calc); //recursively search upwards + } + } + mergeParent = this.mergeCells.mergedCellInfoCollection.getInfo(r, calc.endColumn); + if (mergeParent) { + var mergeEnd = mergeParent.col + mergeParent.colspan - 1; + if (mergeEnd > calc.endColumn) { + calc.endColumn = mergeEnd; + return afterViewportColumnCalculatorOverride.call(this, calc); //recursively search upwards + } + } + } + } +}; + +var isMultipleSelection = function (isMultiple) { + if (isMultiple && this.mergeCells) { + var mergedCells = this.mergeCells.mergedCellInfoCollection + , selectionRange = this.getSelectedRange(); + + for (var group in mergedCells) { + if (selectionRange.highlight.row == mergedCells[group].row && selectionRange.highlight.col == mergedCells[group].col && + selectionRange.to.row == mergedCells[group].row + mergedCells[group].rowspan - 1 && + selectionRange.to.col == mergedCells[group].col + mergedCells[group].colspan - 1) { + return false; + } + } + } + return isMultiple; +}; + +Handsontable.hooks.add('beforeInit', beforeInit); +Handsontable.hooks.add('afterInit', afterInit); +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('beforeDrawBorders', beforeDrawAreaBorders); +Handsontable.hooks.add('afterIsMultipleSelection', isMultipleSelection); +Handsontable.hooks.add('afterRenderer', afterRenderer); +Handsontable.hooks.add('afterContextMenuDefaultOptions', addMergeActionsToContextMenu); +Handsontable.hooks.add('afterGetCellMeta', afterGetCellMeta); +Handsontable.hooks.add('afterViewportRowCalculatorOverride', afterViewportRowCalculatorOverride); +Handsontable.hooks.add('afterViewportColumnCalculatorOverride', afterViewportColumnCalculatorOverride); + +Handsontable.MergeCells = MergeCells; + + +(function () { + + function CustomBorders () { + + } + +// /*** +// * Array for all custom border objects (for redraw) +// * @type {{}} +// */ +// var bordersArray = {}, + /*** + * Current instance (table where borders should be placed) + */ + var instance; + + + /*** + * Check if plugin should be enabled + */ + var checkEnable = function (customBorders) { + if(typeof customBorders === "boolean"){ + if (customBorders === true){ + return true; + } + } + + if(typeof customBorders === "object"){ + if(customBorders.length > 0) { + return true; + } + } + return false; + }; + + + /*** + * Initialize plugin + */ + var init = function () { + + if(checkEnable(this.getSettings().customBorders)){ + if(!this.customBorders){ + instance = this; + this.customBorders = new CustomBorders(); + } + } + }; + + /*** + * get index of border setting + * @param className + * @returns {number} + */ + var getSettingIndex = function (className) { + for (var i = 0; i < instance.view.wt.selections.length; i++){ + if (instance.view.wt.selections[i].settings.className == className){ + return i; + } + } + return -1; + }; + + /*** + * Insert WalkontableSelection instance into Walkontable.settings + * @param border + */ + var insertBorderIntoSettings = function (border) { + var coordinates = { + row: border.row, + col: border.col + }; + var selection = new WalkontableSelection(border, new WalkontableCellRange(coordinates, coordinates, coordinates)); + var index = getSettingIndex(border.className); + + if(index >=0) { + instance.view.wt.selections[index] = selection; + } else { + instance.view.wt.selections.push(selection); + } + }; + + /*** + * 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); + + insertBorderIntoSettings(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); + insertBorderIntoSettings(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; + }; + + /*** + * Remove borders divs from DOM + * + * @param borderClassName + */ + var removeBordersFromDom = function (borderClassName) { + var borders = document.querySelectorAll("." + borderClassName); + + for(var i = 0; i< borders.length; i++) { + if (borders[i]) { + if(borders[i].nodeName != 'TD') { + var parent = borders[i].parentNode; + if(parent.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); + this.removeCellMeta(row, col, 'borders'); + }; + + /*** + * 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; + /* jshint ignore:start */ + if (!bordersMeta || bordersMeta.border == undefined){ + bordersMeta = createEmptyBorders(row, col); + } + /* jshint ignore:end */ + + if (remove) { + bordersMeta[place] = createSingleEmptyBorder(); + } else { + bordersMeta[place] = createDefaultCustomBorder(); + } + + this.setCellMeta(row, col, 'borders', bordersMeta); + + var borderClassName = createClassName(row,col); + removeBordersFromDom(borderClassName); + insertBorderIntoSettings(bordersMeta); + + 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 "" + String.fromCharCode(10003) + "" + label; // workaround for https://github.com/handsontable/handsontable/issues/1946 + }; + + /*** + * Add border options to context menu + * + * @param defaultOptions + */ + var addBordersOptionsToContextMenu = function (defaultOptions) { + if(!this.getSettings().customBorders){ + return; + } + + defaultOptions.items.push(Handsontable.ContextMenu.SEPARATOR); + + defaultOptions.items.push({ + key: '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, 'left'); + 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('afterInit', function () { + var customBorders = this.getSettings().customBorders; + + if (customBorders){ + + 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]); + } + } + + this.render(); + this.view.wt.draw(true); + } + + }); + + 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'), + eventManager = Handsontable.eventManager(this); + + 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.appendChild(handle); + } + } + + function refreshHandlePosition(TH, delta) { + var box = TH.getBoundingClientRect(); + var handleHeight = 6; + if (delta > 0) { + handle.style.top = (box.top + box.height - handleHeight) + 'px'; + } + else { + 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.appendChild(guide); + } + + function refreshGuidePosition(diff) { + guide.style.top = startOffset + diff + 'px'; + } + + function hideHandleAndGuide() { + Handsontable.Dom.removeClass(handle, 'active'); + Handsontable.Dom.removeClass(guide, 'active'); + } + + var checkRowHeader = function (element) { + if (element.tagName != 'BODY') { + if (element.parentNode.tagName == 'TBODY') { + return true; + } else { + element = element.parentNode; + return checkRowHeader(element); + } + } + return false; + }; + + var getTHFromTargetElement = function (element) { + if (element.tagName != 'TABLE') { + if (element.tagName == 'TH') { + return element; + } else { + return getTHFromTargetElement(element.parentNode); + } + } + return null; + }; + + var bindEvents = function () { + var instance = this; + var pressed; + + + eventManager.addEventListener(instance.rootElement, 'mouseover', function (e) { + if (checkRowHeader(e.target)) { + var th = getTHFromTargetElement(e.target); + if (th) { + if (pressed) { + endRow = instance.view.wt.wtTable.getCoords(th).row; + refreshHandlePosition(th, endRow - startRow); + } + else { + setupHandlePosition.call(instance, th); + } + } + } + }); + + eventManager.addEventListener(instance.rootElement, 'mousedown', function (e) { + if (Handsontable.Dom.hasClass(e.target, 'manualRowMover')) { + startY = Handsontable.helper.pageY(e); + setupGuidePosition.call(instance); + pressed = instance; + + startRow = currentRow; + endRow = currentRow; + } + }); + + eventManager.addEventListener(window, 'mousemove', function (e) { + if (pressed) { + refreshGuidePosition(Handsontable.helper.pageY(e) - startY); + } + }); + + eventManager.addEventListener(window, 'mouseup', function (e) { + if (pressed) { + hideHandleAndGuide(); + pressed = false; + + 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 () { + eventManager.clear(); + }; + + 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); + + // update plugin usages count for manualColumnPositions + if (typeof instance.manualRowPositionsPluginUsages != 'undefined') { + instance.manualRowPositionsPluginUsages.push('manualColumnMove'); + } else { + instance.manualRowPositionsPluginUsages = ['manualColumnMove']; + } + + if (typeof loadedManualRowPostions != 'undefined') { + this.manualRowPositions = loadedManualRowPostions; + } else if (Array.isArray(initialManualRowPositions)) { + this.manualRowPositions = initialManualRowPositions; + } else { + this.manualRowPositions = []; + } + + if (source === 'afterInit') { + bindEvents.call(this); + if (this.manualRowPositions.length > 0) { + instance.forceFullRender = true; + instance.render(); + } + } + } else { + var pluginUsagesIndex = instance.manualRowPositionsPluginUsages ? instance.manualRowPositionsPluginUsages.indexOf('manualColumnMove') : -1; + if (pluginUsagesIndex > -1) { + unbindEvents.call(this); + instance.manualRowPositions = []; + instance.manualRowPositionsPluginUsages[pluginUsagesIndex] = void 0; + } + } + + }; + + 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 wtOnCellCornerMouseDown, + wtOnCellMouseOver, + mouseDownOnCellCorner = false, + plugin = this, + eventManager = Handsontable.eventManager(instance); + + + var mouseUpCallback = function (event) { + if (!instance.autofill) { + return true; + } + + if (instance.autofill.handle && instance.autofill.handle.isDragged) { + if (instance.autofill.handle.isDragged > 1) { + instance.autofill.apply(); + } + instance.autofill.handle.isDragged = 0; + mouseDownOnCellCorner = false; + } + }; + + eventManager.addEventListener(document, 'mouseup', function (event) { + mouseUpCallback(event); + }); + + eventManager.addEventListener(document,'mousemove', function (event){ + if (!plugin.instance.autofill) { + return 0; + } + + var tableBottom = Handsontable.Dom.offset(plugin.instance.table).top - (window.pageYOffset || document.documentElement.scrollTop) + Handsontable.Dom.outerHeight(plugin.instance.table) + , tableRight = Handsontable.Dom.offset(plugin.instance.table).left - (window.pageXOffset || document.documentElement.scrollLeft) + Handsontable.Dom.outerWidth(plugin.instance.table); + + + if (plugin.addingStarted === false && plugin.instance.autofill.handle.isDragged > 0 && event.clientY > tableBottom && event.clientX <= tableRight) { // dragged outside bottom + this.mouseDragOutside = true; + plugin.addingStarted = true; + } else { + this.mouseDragOutside = false; + } + + if (this.mouseDragOutside) { + setTimeout(function () { + plugin.addingStarted = false; + plugin.instance.alter('insert_row'); + }, 200); + } + }); + + /* + * 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; + mouseDownOnCellCorner = true; + + 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 && (mouseDownOnCellCorner && !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; + } + + var getDeltas = function (start, end, data, direction) { + var rlength = data.length, // rows + clength = data ? data[0].length : 0; // cols + + var deltas = []; + + var diffRow = end.row - start.row, + diffCol = end.col - start.col; + + var startValue, endValue, delta; + + var arr = []; + + if (['down', 'up'].indexOf(direction) !== -1) { + for (var col = 0; col <= diffCol; col++) { + + startValue = parseInt(data[0][col], 10); + endValue = parseInt(data[rlength-1][col], 10); + delta = (direction === 'down' ? (endValue - startValue) : (startValue - endValue)) / (rlength - 1) || 0; + + arr.push(delta); + } + + deltas.push(arr); + } + + if (['right', 'left'].indexOf(direction) !== -1) { + for (var row = 0; row <= diffRow; row++) { + + startValue = parseInt(data[row][0], 10); + endValue = parseInt(data[row][clength-1], 10); + delta = (direction === 'right' ? (endValue - startValue) : (startValue - endValue)) / (clength - 1) || 0; + + arr = []; + arr.push(delta); + + deltas.push(arr); + } + } + + return deltas; + }; + + 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(); + } + + var direction; + + if (drag[0] === select[0] && drag[1] < select[1]) { + direction = 'left'; + + 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]) { + direction = 'right'; + + 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]) { + direction = 'up'; + + 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]) { + direction = 'down'; + + start = new WalkontableCellCoords( + select[2] + 1, + drag[1] + ); + end = new WalkontableCellCoords( + drag[2], + drag[3] + ); + } + + if (start && start.row > -1 && start.col > -1) { + 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); + + var deltas = getDeltas(start, end, _data, direction); + + Handsontable.hooks.run(this.instance, 'beforeAutofill', start, end, _data); + + this.instance.populateFromArray(start.row, start.col, _data, end.row, end.col, 'autofill', null, direction, deltas); + + 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 + this.instance.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, + selection, + tableRows = this.instance.countRows(), + that = this; + + if (this.instance.view.wt.selections.fill.cellRange && this.addingStarted === false) { + selection = this.instance.getSelected(); + fillCorners = this.instance.view.wt.selections.fill.getCorners(); + + if (selection[2] < tableRows - 1 && 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); + +var Grouping = function (instance) { + /** + * array of items + * @type {Array} + */ + var groups = []; + + /** + * group definition + * @type {{id: String, level: Number, rows: Array, cols: Array, hidden: Number}} + */ + var item = { + id: '', + level: 0, + hidden: 0, + rows: [], + cols: [] + }; + + /** + * total rows and cols merged in groups + * @type {{rows: number, cols: number}} + */ + var counters = { + rows: 0, + cols: 0 + }; + + /** + * Number of group levels in each dimension + * @type {{rows: number, cols: number}} + */ + var levels = { + rows: 0, + cols: 0 + }; + + /** + * List of hidden rows + * @type {Array} + */ + var hiddenRows = []; + + /** + * List of hidden columns + * @type {Array} + */ + var hiddenCols = []; + + /** + * Classes used + */ + var classes = { + 'groupIndicatorContainer': 'htGroupIndicatorContainer', + 'groupIndicator': function (direction) { + return 'ht' + direction + 'Group'; + }, + 'groupStart': 'htGroupStart', + 'collapseButton': 'htCollapseButton', + 'expandButton': 'htExpandButton', + 'collapseGroupId': function (id) { + return 'htCollapse-' + id; + }, + 'collapseFromLevel': function (direction, level) { + return 'htCollapse' + direction + 'FromLevel-' + level; + }, + 'clickable': 'clickable', + 'levelTrigger': 'htGroupLevelTrigger' + }; + + /** + * compare object properties + * @param {String} property + * @param {String} orderDirection + * @returns {Function} + */ + var compare = function (property, orderDirection) { + return function (item1, item2) { + return typeof (orderDirection) === 'undefined' || orderDirection === 'asc' ? item1[property] - item2[property] : item2[property] - item1[property]; + }; + }; + + /** + * Create range array between from and to + * @param {Number} from + * @param {Number} to + * @returns {Array} + */ + var range = function (from, to) { + var arr = []; + while (from <= to) { + arr.push(from++); + } + + return arr; + }; + + /** + * * Get groups for range + * @param from + * @param to + * @returns {{total: {rows: number, cols: number}, groups: Array}} + */ + var getRangeGroups = function (dataType, from, to) { + var cells = [], + cell = { + row: null, + col: null + }; + + if (dataType == "cols") { + // get all rows for selected columns + while (from <= to) { + cell = { + row: -1, + col: from++ + }; + cells.push(cell); + } + + } else { + // get all columns for selected rows + while (from <= to) { + cell = { + row: from++, + col: -1 + }; + cells.push(cell); + } + } + + var cellsGroups = getCellsGroups(cells), + totalRows = 0, + totalCols = 0; + + // for selected cells, calculate total groups divided into rows and columns + for (var i = 0; i < cellsGroups.length; i++) { + totalRows += cellsGroups[i].filter(function (item) { + return item['rows']; + }).length; + + totalCols += cellsGroups[i].filter(function (item) { + return item['cols']; + }).length; + } + + return { + total: { + rows: totalRows, + cols: totalCols + }, + groups: cellsGroups + }; + }; + + /** + * Get all groups for cells + * @param {Array} cells [{row:0, col:0}, {row:0, col:1}, {row:1, col:2}] + * @returns {Array} + */ + var getCellsGroups = function (cells) { + var _groups = []; + + for (var i = 0; i < cells.length; i++) { + _groups.push(getCellGroups(cells[i])); + } + + return _groups; + }; + + /** + * Get all groups for cell + * @param {Object} coords {row:1, col:2} + * @param {Number} groupLevel Optional + * @param {String} groupType Optional + * @returns {Array} + */ + var getCellGroups = function (coords, groupLevel, groupType) { + var row = coords.row, + col = coords.col; + + // for row = -1 and col = -1, get all columns and rows + var tmpRow = (row === -1 ? 0 : row), + tmpCol = (col === -1 ? 0 : col); + + var _groups = []; + + for (var i = 0; i < groups.length; i++) { + var group = groups[i], + id = group['id'], + level = group['level'], + rows = group['rows'] || [], + cols = group['cols'] || []; + + if (_groups.indexOf(id) === -1) { + if (rows.indexOf(tmpRow) !== -1 || cols.indexOf(tmpCol) !== -1) { + _groups.push(group); + } + } + } + + // add col groups + if (col === -1) { + _groups = _groups.concat(getColGroups()); + } else if (row === -1) { + // add row groups + _groups = _groups.concat(getRowGroups()); + } + + if (groupLevel) { + _groups = _groups.filter(function (item) { + return item['level'] === groupLevel; + }); + } + + if (groupType) { + if (groupType === 'cols') { + _groups = _groups.filter(function (item) { + return item['cols']; + }); + } else if (groupType === 'rows') { + _groups = _groups.filter(function (item) { + return item['rows']; + }); + } + } + + // remove duplicates + var tmp = []; + return _groups.filter(function (item) { + if (tmp.indexOf(item.id) === -1) { + tmp.push(item.id); + return item; + } + }); + }; + + /** + * get group by id + * @param id + * @returns {Object} group + */ + var getGroupById = function (id) { + for (var i = 0, groupsLength = groups.length; i < groupsLength; i++) { + if (groups[i].id == id) { + return groups[i]; + } + } + return false; + }; + + /** + * get group by row and level + * @param row + * @param level + * @returns {Object} group + */ + var getGroupByRowAndLevel = function (row, level) { + for (var i = 0, groupsLength = groups.length; i < groupsLength; i++) { + if (groups[i].level == level && groups[i].rows && groups[i].rows.indexOf(row) > -1) { + return groups[i]; + } + } + return false; + }; + + /** + * get group by row and level + * @param row + * @param level + * @returns {Object} group + */ + var getGroupByColAndLevel = function (col, level) { + for (var i = 0, groupsLength = groups.length; i < groupsLength; i++) { + if (groups[i].level == level && groups[i].cols && groups[i].cols.indexOf(col) > -1) { + return groups[i]; + } + } + return false; + }; + + /** + * get total column groups + * @returns {*|Array} + */ + var getColGroups = function () { + var result = []; + for (var i = 0, groupsLength = groups.length; i < groupsLength; i++) { + if (Array.isArray(groups[i]['cols'])) { + result.push(groups[i]); + } + } + return result; + }; + + /** + * get total col groups by level + * @param {Number} level + * @returns {*|Array} + */ + var getColGroupsByLevel = function (level) { + var result = []; + for (var i = 0, groupsLength = groups.length; i < groupsLength; i++) { + if (groups[i]['cols'] && groups[i]['level'] === level) { + result.push(groups[i]); + } + } + return result; + }; + + /** + * get total row groups + * @returns {*|Array} + */ + var getRowGroups = function () { + var result = []; + for (var i = 0, groupsLength = groups.length; i < groupsLength; i++) { + if (Array.isArray(groups[i]['rows'])) { + result.push(groups[i]); + } + } + return result; + }; + + /** + * get total row groups by level + * @param {Number} level + * @returns {*|Array} + */ + var getRowGroupsByLevel = function (level) { + var result = []; + for (var i = 0, groupsLength = groups.length; i < groupsLength; i++) { + if (groups[i]['rows'] && groups[i]['level'] === level) { + result.push(groups[i]); + } + } + return result; + }; + + /** + * get last inserted range level in columns + * @param {Array} rangeGroups + * @returns {number} + */ + var getLastLevelColsInRange = function (rangeGroups) { + var level = 0; + + if (rangeGroups.length) { + rangeGroups.forEach(function (items) { + items = items.filter(function (item) { + return item['cols']; + }); + + if (items.length) { + var sortedGroup = items.sort(compare('level', 'desc')), + lastLevel = sortedGroup[0].level; + + if (level < lastLevel) { + level = lastLevel; + } + } + }); + } + + return level; + }; + + /** + * get last inserted range level in rows + * @param {Array} rangeGroups + * @returns {number} + */ + var getLastLevelRowsInRange = function (rangeGroups) { + var level = 0; + + if (rangeGroups.length) { + rangeGroups.forEach(function (items) { + items = items.filter(function (item) { + return item['rows']; + }); + + if (items.length) { + var sortedGroup = items.sort(compare('level', 'desc')), + lastLevel = sortedGroup[0].level; + + if (level < lastLevel) { + level = lastLevel; + } + } + }); + } + + return level; + }; + + /** + * create group for cols + * @param {Number} from + * @param {Number} to + */ + var groupCols = function (from, to) { + var rangeGroups = getRangeGroups("cols", from, to), + lastLevel = getLastLevelColsInRange(rangeGroups.groups); + + if (lastLevel === levels.cols) { + levels.cols++; + } else if (lastLevel > levels.cols) { + levels.cols = lastLevel + 1; + } + + if (!counters.cols) { + counters.cols = getColGroups().length; + } + + counters.cols++; + groups.push({ + id: 'c' + counters.cols, + level: lastLevel + 1, + cols: range(from, to), + hidden: 0 + }); + }; + + /** + * create group for rows + * @param {Number} from + * @param {Number} to + */ + var groupRows = function (from, to) { + var rangeGroups = getRangeGroups("rows", from, to), + lastLevel = getLastLevelRowsInRange(rangeGroups.groups); + + levels.rows = Math.max(levels.rows, lastLevel + 1); + + + if (!counters.rows) { + counters.rows = getRowGroups().length; + } + + counters.rows++; + groups.push({ + id: 'r' + counters.rows, + level: lastLevel + 1, + rows: range(from, to), + hidden: 0 + }); + }; + + /** + * show or hide groups + * @param showHide + * @param groups + */ + var showHideGroups = function (hidden, groups) { + var level; + for (var i = 0, groupsLength = groups.length; i < groupsLength; i++) { + groups[i].hidden = hidden; + level = groups[i].level; + + if (!hiddenRows[level]) { + hiddenRows[level] = []; + } + if (!hiddenCols[level]) { + hiddenCols[level] = []; + } + + if (groups[i].rows) { + for (var j = 0, rowsLength = groups[i].rows.length; j < rowsLength; j++) { + if (hidden > 0) { + hiddenRows[level][groups[i].rows[j]] = true; + } else { + hiddenRows[level][groups[i].rows[j]] = void 0; + } + } + } else if (groups[i].cols) { + for (var j = 0, colsLength = groups[i].cols.length; j < colsLength; j++) { + if (hidden > 0) { + hiddenCols[level][groups[i].cols[j]] = true; + } else { + hiddenCols[level][groups[i].cols[j]] = void 0; + } + } + } + } + }; + + /** + * Check if the next cell of the dimension (row / column) contains a group at the same level + * @param dimension + * @param currentPosition + * @param level + * @param currentGroupId + * @returns {boolean} + */ + var nextIndexSharesLevel = function (dimension, currentPosition, level, currentGroupId) { + var nextCellGroupId + , levelsByOrder; + + switch (dimension) { + case 'rows': + nextCellGroupId = getGroupByRowAndLevel(currentPosition + 1, level).id; + levelsByOrder = Handsontable.Grouping.getGroupLevelsByRows(); + break; + case 'cols': + nextCellGroupId = getGroupByColAndLevel(currentPosition + 1, level).id; + levelsByOrder = Handsontable.Grouping.getGroupLevelsByCols(); + break; + } + + return !!(levelsByOrder[currentPosition + 1] && levelsByOrder[currentPosition + 1].indexOf(level) > -1 && currentGroupId == nextCellGroupId); + + }; + + /** + * Check if the previous cell of the dimension (row / column) contains a group at the same level + * @param dimension + * @param currentPosition + * @param level + * @param currentGroupId + * @returns {boolean} + */ + var previousIndexSharesLevel = function (dimension, currentPosition, level, currentGroupId) { + var previousCellGroupId + , levelsByOrder; + + switch (dimension) { + case 'rows': + previousCellGroupId = getGroupByRowAndLevel(currentPosition - 1, level).id; + levelsByOrder = Handsontable.Grouping.getGroupLevelsByRows(); + break; + case 'cols': + previousCellGroupId = getGroupByColAndLevel(currentPosition - 1, level).id; + levelsByOrder = Handsontable.Grouping.getGroupLevelsByCols(); + break; + } + + return !!(levelsByOrder[currentPosition - 1] && levelsByOrder[currentPosition - 1].indexOf(level) > -1 && currentGroupId == previousCellGroupId); + + }; + + /** + * Check if the provided index is at the end of the group indicator line + * @param dimension + * @param index + * @param level + * @param currentGroupId + * @returns {boolean} + */ + var isLastIndexOfTheLine = function (dimension, index, level, currentGroupId) { + if (index === 0) { + return false; + } + var levelsByOrder + , entriesLength + , previousSharesLevel = previousIndexSharesLevel(dimension, index, level, currentGroupId) + , nextSharesLevel = nextIndexSharesLevel(dimension, index, level, currentGroupId) + , nextIsHidden = false; + + switch (dimension) { + case 'rows': + levelsByOrder = Handsontable.Grouping.getGroupLevelsByRows(); + entriesLength = instance.countRows(); + for (var i = 0; i <= levels.rows; i++) { + if (hiddenRows[i] && hiddenRows[i][index + 1]) { + nextIsHidden = true; + break; + } + } + break; + case 'cols': + levelsByOrder = Handsontable.Grouping.getGroupLevelsByCols(); + entriesLength = instance.countCols(); + for (var i = 0; i <= levels.cols; i++) { + if (hiddenCols[i] && hiddenCols[i][index + 1]) { + nextIsHidden = true; + break; + } + } + break; + } + + if (previousSharesLevel) { + if (index == entriesLength - 1) { + return true; + } else if (!nextSharesLevel || (nextSharesLevel && nextIsHidden)) { + return true; + } else if (!levelsByOrder[index + 1]) { + return true; + } + } + return false; + }; + + /** + * Check if all rows/cols are hidden + * @param dataType + */ + var isLastHidden = function (dataType) { + var levelAmount; + + switch (dataType) { + case 'rows': + levelAmount = levels.rows; + for (var j = 0; j <= levelAmount; j++) { + if (hiddenRows[j] && hiddenRows[j][instance.countRows() - 1]) { + return true; + } + } + + break; + case 'cols': + levelAmount = levels.cols; + for (var j = 0; j <= levelAmount; j++) { + if (hiddenCols[j] && hiddenCols[j][instance.countCols() - 1]) { + return true; + } + } + break; + } + + return false; + }; + + /** + * Check if the provided index is at the beginning of the group indicator line + * @param dimension + * @param index + * @param level + * @param currentGroupId + * @returns {boolean} + */ + var isFirstIndexOfTheLine = function (dimension, index, level, currentGroupId) { + var levelsByOrder + , entriesLength + , currentGroup = getGroupById(currentGroupId) + , previousAreHidden = false + , arePreviousHidden = function (dimension) { + var hidden = false + , hiddenArr = dimension == 'rows' ? hiddenRows : hiddenCols; + for (var i = 0; i <= levels[dimension]; i++) { + tempInd = index; + while (currentGroup[dimension].indexOf(tempInd) > -1) { + hidden = !!(hiddenArr[i] && hiddenArr[i][tempInd]); + tempInd--; + } + if (hidden) { + break; + } + } + return hidden; + } + , previousSharesLevel = previousIndexSharesLevel(dimension, index, level, currentGroupId) + , nextSharesLevel = nextIndexSharesLevel(dimension, index, level, currentGroupId) + , tempInd; + + switch (dimension) { + case 'rows': + levelsByOrder = Handsontable.Grouping.getGroupLevelsByRows(); + entriesLength = instance.countRows(); + previousAreHidden = arePreviousHidden(dimension); + break; + case 'cols': + levelsByOrder = Handsontable.Grouping.getGroupLevelsByCols(); + entriesLength = instance.countCols(); + previousAreHidden = arePreviousHidden(dimension); + break; + } + + if (index == entriesLength - 1) { + return false; + } + else if (index === 0) { + if (nextSharesLevel) { + return true; + } + } else if (!previousSharesLevel || (previousSharesLevel && previousAreHidden)) { + if (nextSharesLevel) { + return true; + } + } else if (!levelsByOrder[index - 1]) { + if (nextSharesLevel) { + return true; + } + } + return false; + }; + + /** + * Add group expander button + * @param dimension + * @param index + * @param level + * @param id + * @param elem + * @returns {*} + */ + var addGroupExpander = function (dataType, index, level, id, elem) { + var previousIndexGroupId; + + switch (dataType) { + case 'rows': + previousIndexGroupId = getGroupByRowAndLevel(index - 1, level).id; + break; + case 'cols': + previousIndexGroupId = getGroupByColAndLevel(index - 1, level).id; + break; + } + + if (!previousIndexGroupId) { + return null; + } + + if (index > 0) { + if (previousIndexSharesLevel(dataType, index - 1, level, previousIndexGroupId) && previousIndexGroupId != id) { + + var expanderButton = document.createElement('DIV'); + Handsontable.Dom.addClass(expanderButton, classes.expandButton); + expanderButton.id = 'htExpand-' + previousIndexGroupId; + expanderButton.appendChild(document.createTextNode('+')); + expanderButton.setAttribute('data-level', level); + expanderButton.setAttribute('data-type', dataType); + expanderButton.setAttribute('data-hidden', "1"); + + elem.appendChild(expanderButton); + + return expanderButton; + } + } + return null; + }; + + /** + * Check if provided cell is collapsed (either by rows or cols) + * @param currentPosition + * @returns {boolean} + */ + var isCollapsed = function (currentPosition) { + var rowGroups = getRowGroups() + , colGroups = getColGroups(); + + for (var i = 0, rowGroupsCount = rowGroups.length; i < rowGroupsCount; i++) { + if (rowGroups[i].rows.indexOf(currentPosition.row) > -1 && rowGroups[i].hidden) { + return true; + } + } + + if (currentPosition.col === null) { // if col is set to null, check only rows + return false; + } + + for (var i = 0, colGroupsCount = colGroups.length; i < colGroupsCount; i++) { + if (colGroups[i].cols.indexOf(currentPosition.col) > -1 && colGroups[i].hidden) { + return true; + } + } + + return false; + }; + + return { + + /** + * all groups for ht instance + */ + getGroups: function () { + return groups; + }, + /** + * All levels for rows and cols respectively + */ + getLevels: function () { + return levels; + }, + /** + * Current instance + */ + instance: instance, + /** + * Initial setting for minSpareRows + */ + baseSpareRows: instance.getSettings().minSpareRows, + /** + * Initial setting for minSpareCols + */ + baseSpareCols: instance.getSettings().minSpareCols, + + getRowGroups: getRowGroups, + getColGroups: getColGroups, + /** + * init group + * @param {Object} settings, could be an array of objects [{cols: [0,1,2]}, {cols: [3,4,5]}, {rows: [0,1]}] + */ + init: function () { + var groupsSetting = instance.getSettings().groups; + if (groupsSetting) { + if (Array.isArray(groupsSetting)) { + Handsontable.Grouping.initGroups(groupsSetting); + } + } + }, + + /** + * init groups from configuration on startup + */ + initGroups: function (initialGroups) { + var that = this; + + groups = []; + + initialGroups.forEach(function (item) { + var _group = [], + isRow = false, + isCol = false; + + if (Array.isArray(item.rows)) { + _group = item.rows; + isRow = true; + } else if (Array.isArray(item.cols)) { + _group = item.cols; + isCol = true; + } + + var from = _group[0], + to = _group[_group.length - 1]; + + if (isRow) { + groupRows(from, to); + } else if (isCol) { + groupCols(from, to); + } + }); +// this.render(); + }, + + /** + * Remove all existing groups + */ + resetGroups: function () { + groups = []; + counters = { + rows: 0, + cols: 0 + }; + levels = { + rows: 0, + cols: 0 + }; + + var allOccurrences; + for (var i in classes) { + if (typeof classes[i] != 'function') { + allOccurrences = document.querySelectorAll('.' + classes[i]); + for (var j = 0, occurrencesLength = allOccurrences.length; j < occurrencesLength; j++) { + Handsontable.Dom.removeClass(allOccurrences[j], classes[i]); + } + } + } + + var otherClasses = ['htGroupColClosest', 'htGroupCol']; + for (var i = 0, otherClassesLength = otherClasses.length; i < otherClassesLength; i++) { + allOccurrences = document.querySelectorAll('.' + otherClasses[i]); + for (var j = 0, occurrencesLength = allOccurrences.length; j < occurrencesLength; j++) { + Handsontable.Dom.removeClass(allOccurrences[j], otherClasses[i]); + } + } + }, + /** + * Update groups from the instance settings + */ + updateGroups: function () { + var groupSettings = this.getSettings().groups; + + Handsontable.Grouping.resetGroups(); + Handsontable.Grouping.initGroups(groupSettings); + }, + afterGetRowHeader: function (row, TH) { + var currentRowHidden = false; + for (var i = 0, levels = hiddenRows.length; i < levels; i++) { + if (hiddenRows[i] && hiddenRows[i][row] === true) { + currentRowHidden = true; + } + } + + if (currentRowHidden) { + Handsontable.Dom.addClass(TH.parentNode, 'hidden'); + } else if (!currentRowHidden && Handsontable.Dom.hasClass(TH.parentNode, 'hidden')) { + Handsontable.Dom.removeClass(TH.parentNode, 'hidden'); + } + + }, + afterGetColHeader: function (col, TH) { + var rowHeaders = this.view.wt.wtSettings.getSetting('rowHeaders').length + , thisColgroup = instance.rootElement.querySelectorAll('colgroup col:nth-child(' + parseInt(col + rowHeaders + 1, 10) + ')'); + + if (thisColgroup.length === 0) { + return; + } + + var currentColHidden = false; + for (var i = 0, levels = hiddenCols.length; i < levels; i++) { + if (hiddenCols[i] && hiddenCols[i][col] === true) { + currentColHidden = true; + } + } + + if (currentColHidden) { + for (var i = 0, colsAmount = thisColgroup.length; i < colsAmount; i++) { + Handsontable.Dom.addClass(thisColgroup[i], 'hidden'); + } + } else if (!currentColHidden && Handsontable.Dom.hasClass(thisColgroup[0], 'hidden')) { + for (var i = 0, colsAmount = thisColgroup.length; i < colsAmount; i++) { + Handsontable.Dom.removeClass(thisColgroup[i], 'hidden'); + } + } + }, + /** + * Create a renderer for additional row/col headers, acting as group indicators + * @param walkontableConfig + * @param direction + */ + groupIndicatorsFactory: function (renderersArr, direction) { + var groupsLevelsList + , getCurrentLevel + , getCurrentGroupId + , dataType + , getGroupByIndexAndLevel + , headersType + , currentHeaderModifier + , createLevelTriggers; + + switch (direction) { + case 'horizontal': + groupsLevelsList = Handsontable.Grouping.getGroupLevelsByCols(); + getCurrentLevel = function (elem) { + return Array.prototype.indexOf.call(elem.parentNode.parentNode.childNodes, elem.parentNode) + 1; + }; + getCurrentGroupId = function (col, level) { + return getGroupByColAndLevel(col, level).id; + }; + dataType = 'cols'; + getGroupByIndexAndLevel = function (col, level) { + return getGroupByColAndLevel(col - 1, level); + }; + headersType = "columnHeaders"; + currentHeaderModifier = function (headerRenderers) { + if (headerRenderers.length === 1) { + var oldFn = headerRenderers[0]; + + headerRenderers[0] = function (index, elem, level) { + + if (index < -1) { + makeGroupIndicatorsForLevel()(index, elem, level); + } else { + Handsontable.Dom.removeClass(elem, classes.groupIndicatorContainer); + oldFn(index, elem, level); + } + }; + } + return function () { + return headerRenderers; + }; + }; + createLevelTriggers = true; + break; + case 'vertical': + groupsLevelsList = Handsontable.Grouping.getGroupLevelsByRows(); + getCurrentLevel = function (elem) { + return Handsontable.Dom.index(elem) + 1; + }; + getCurrentGroupId = function (row, level) { + return getGroupByRowAndLevel(row, level).id; + }; + dataType = 'rows'; + getGroupByIndexAndLevel = function (row, level) { + return getGroupByRowAndLevel(row - 1, level); + }; + headersType = "rowHeaders"; + currentHeaderModifier = function (headerRenderers) { + return headerRenderers; + }; + break; + } + + var createButton = function (parent) { + var button = document.createElement('div'); + + parent.appendChild(button); + + return { + button: button, + addClass: function (className) { + Handsontable.Dom.addClass(button, className); + } + }; + }; + + var makeGroupIndicatorsForLevel = function () { + var directionClassname = direction.charAt(0).toUpperCase() + direction.slice(1); // capitalize the first letter + + return function (index, elem, level) { // header rendering function + + level++; + var child + , collapseButton; + + /* jshint -W084 */ + while (child = elem.lastChild) { + elem.removeChild(child); + } + + Handsontable.Dom.addClass(elem, classes.groupIndicatorContainer); + + var currentGroupId = getCurrentGroupId(index, level); + + if (index > -1 && (groupsLevelsList[index] && groupsLevelsList[index].indexOf(level) > -1)) { + + collapseButton = createButton(elem); + collapseButton.addClass(classes.groupIndicator(directionClassname)); + + if (isFirstIndexOfTheLine(dataType, index, level, currentGroupId)) { // add a little thingy and the top of the group indicator + collapseButton.addClass(classes.groupStart); + } + + if (isLastIndexOfTheLine(dataType, index, level, currentGroupId)) { // add [+]/[-] button at the end of the line + collapseButton.button.appendChild(document.createTextNode('-')); + collapseButton.addClass(classes.collapseButton); + collapseButton.button.id = classes.collapseGroupId(currentGroupId); + collapseButton.button.setAttribute('data-level', level); + collapseButton.button.setAttribute('data-type', dataType); + } + + } + + if (createLevelTriggers) { + var rowInd = Handsontable.Dom.index(elem.parentNode); + if (index === -1 || (index < -1 && rowInd === Handsontable.Grouping.getLevels().cols + 1) || + (rowInd === 0 && Handsontable.Grouping.getLevels().cols === 0)) { + collapseButton = createButton(elem); + collapseButton.addClass(classes.levelTrigger); + + if (index === -1) { + collapseButton.button.id = classes.collapseFromLevel("Cols", level); + collapseButton.button.appendChild(document.createTextNode(level)); + } else if (index < -1 && rowInd === Handsontable.Grouping.getLevels().cols + 1 || + (rowInd === 0 && Handsontable.Grouping.getLevels().cols === 0)) { + var colInd = Handsontable.Dom.index(elem) + 1; + collapseButton.button.id = classes.collapseFromLevel("Rows", colInd); + collapseButton.button.appendChild(document.createTextNode(colInd)); + } + } + } + + // add group expending button + var expanderButton = addGroupExpander(dataType, index, level, currentGroupId, elem); + if (index > 0) { + var previousGroupObj = getGroupByIndexAndLevel(index - 1, level); + + if (expanderButton && previousGroupObj.hidden) { + Handsontable.Dom.addClass(expanderButton, classes.clickable); + } + } + + updateHeaderWidths(); + + }; + }; + + + renderersArr = currentHeaderModifier(renderersArr); + + + if (counters[dataType] > 0) { + for (var i = 0; i < levels[dataType] + 1; i++) { // for each level of col groups add a header renderer + if (!Array.isArray(renderersArr)) { + renderersArr = typeof renderersArr === 'function' ? renderersArr() : new Array(renderersArr); + } + renderersArr.unshift(makeGroupIndicatorsForLevel()); + } + } + }, + /** + * Get group levels array arranged by rows + * @returns {Array} + */ + getGroupLevelsByRows: function () { + var rowGroups = getRowGroups() + , result = []; + + for (var i = 0, groupsLength = rowGroups.length; i < groupsLength; i++) { + if (rowGroups[i].rows) { + for (var j = 0, groupRowsLength = rowGroups[i].rows.length; j < groupRowsLength; j++) { + if (!result[rowGroups[i].rows[j]]) { + result[rowGroups[i].rows[j]] = []; + } + result[rowGroups[i].rows[j]].push(rowGroups[i].level); + } + } + } + return result; + }, + /** + * Get group levels array arranged by cols + * @returns {Array} + */ + getGroupLevelsByCols: function () { + var colGroups = getColGroups() + , result = []; + + for (var i = 0, groupsLength = colGroups.length; i < groupsLength; i++) { + if (colGroups[i].cols) { + for (var j = 0, groupColsLength = colGroups[i].cols.length; j < groupColsLength; j++) { + if (!result[colGroups[i].cols[j]]) { + result[colGroups[i].cols[j]] = []; + } + result[colGroups[i].cols[j]].push(colGroups[i].level); + } + } + } + return result; + }, + /** + * Toggle the group visibility ( + / - event handler) + * @param event + * @param coords + * @param TD + */ + toggleGroupVisibility: function (event, coords, TD) { + if (Handsontable.Dom.hasClass(event.target, classes.expandButton) || + Handsontable.Dom.hasClass(event.target, classes.collapseButton) || + Handsontable.Dom.hasClass(event.target, classes.levelTrigger)) { + var element = event.target + , elemIdSplit = element.id.split('-'); + + var groups = [] + , id + , level + , type + , hidden; + + var prepareGroupData = function (componentElement) { + if (componentElement) { + element = componentElement; + } + + elemIdSplit = element.id.split('-'); + + id = elemIdSplit[1]; + level = parseInt(element.getAttribute('data-level'), 10); + type = element.getAttribute('data-type'); + hidden = parseInt(element.getAttribute('data-hidden')); + + if (isNaN(hidden)) { + hidden = 1; + } else { + hidden = (hidden ? 0 : 1); + } + + element.setAttribute('data-hidden', hidden.toString()); + + + groups.push(getGroupById(id)); + }; + + if (element.className.indexOf(classes.levelTrigger) > -1) { // show levels below, hide all above + var groupsInLevel + , groupsToExpand = [] + , groupsToCollapse = [] + , levelType = element.id.indexOf("Rows") > -1 ? "rows" : "cols"; + + for (var i = 1, levelsCount = levels[levelType]; i <= levelsCount; i++) { + groupsInLevel = levelType == "rows" ? getRowGroupsByLevel(i) : getColGroupsByLevel(i); + + if (i >= parseInt(elemIdSplit[1], 10)) { + for (var j = 0, groupCount = groupsInLevel.length; j < groupCount; j++) { + groupsToCollapse.push(groupsInLevel[j]); + } + } else { + for (var j = 0, groupCount = groupsInLevel.length; j < groupCount; j++) { + groupsToExpand.push(groupsInLevel[j]); + } + } + } + + showHideGroups(true, groupsToCollapse); + showHideGroups(false, groupsToExpand); + + } else { + prepareGroupData(); + showHideGroups(hidden, groups); + } + + // add the expander button to a dummy spare row/col, if no longer needed -> remove it + /* jshint -W038 */ + type = type || levelType; + + var lastHidden = isLastHidden(type) + , typeUppercase = type.charAt(0).toUpperCase() + type.slice(1) + , spareElements = Handsontable.Grouping['baseSpare' + typeUppercase]; + + if (lastHidden) { + /* jshint -W041 */ + if (spareElements == 0) { + instance.alter('insert_' + type.slice(0, -1), instance['count' + typeUppercase]()); + Handsontable.Grouping["dummy" + type.slice(0, -1)] = true; + } + } else { + /* jshint -W041 */ + if (spareElements == 0) { + if (Handsontable.Grouping["dummy" + type.slice(0, -1)]) { + instance.alter('remove_' + type.slice(0, -1), instance['count' + typeUppercase]() - 1); + Handsontable.Grouping["dummy" + type.slice(0, -1)] = false; + } + } + } + + instance.render(); + + event.stopImmediatePropagation(); + } + }, + /** + * Modify the delta when changing cells using keyobard + * @param position + * @returns {Function} + */ + modifySelectionFactory: function (position) { + var instance = this.instance; + var currentlySelected + , nextPosition = new WalkontableCellCoords(0, 0) + , nextVisible = function (direction, currentPosition) { // updates delta to skip to the next visible cell + var updateDelta = 0; + + switch (direction) { + case 'down': + while (isCollapsed(currentPosition)) { + updateDelta++; + currentPosition.row += 1; + } + break; + case 'up': + while (isCollapsed(currentPosition)) { + updateDelta--; + currentPosition.row -= 1; + } + break; + case 'right': + while (isCollapsed(currentPosition)) { + updateDelta++; + currentPosition.col += 1; + } + break; + case 'left': + while (isCollapsed(currentPosition)) { + updateDelta--; + currentPosition.col -= 1; + } + break; + } + + return updateDelta; + } + , updateDelta = function (delta, nextPosition) { + if (delta.row > 0) { // moving down + if (isCollapsed(nextPosition)) { + delta.row += nextVisible('down', nextPosition); + } + } else if (delta.row < 0) { // moving up + if (isCollapsed(nextPosition)) { + delta.row += nextVisible('up', nextPosition); + } + } + + if (delta.col > 0) { // moving right + if (isCollapsed(nextPosition)) { + delta.col += nextVisible('right', nextPosition); + } + } else if (delta.col < 0) { // moving left + if (isCollapsed(nextPosition)) { + delta.col += nextVisible('left', nextPosition); + } + } + }; + + /* jshint -W027 */ + switch (position) { + case 'start': + return function (delta) { + currentlySelected = instance.getSelected(); + nextPosition.row = currentlySelected[0] + delta.row; + nextPosition.col = currentlySelected[1] + delta.col; + + updateDelta(delta, nextPosition); + }; + break; + case 'end': + return function (delta) { + currentlySelected = instance.getSelected(); + nextPosition.row = currentlySelected[2] + delta.row; + nextPosition.col = currentlySelected[3] + delta.col; + + updateDelta(delta, nextPosition); + }; + break; + } + }, + modifyRowHeight: function (height, row) { + if (instance.view.wt.wtTable.rowFilter && isCollapsed({row: row, col: null})) { + return 0; + } + }, + validateGroups: function () { + + var areRangesOverlapping = function (a, b) { + if ((a[0] < b[0] && a[1] < b[1] && b[0] <= a[1]) || + (a[0] > b[0] && b[1] < a[1] && a[0] <= b[1])) { + return true; + } + }; + + var configGroups = instance.getSettings().groups + , cols = [] + , rows = []; + + for (var i = 0, groupsLength = configGroups.length; i < groupsLength; i++) { + if (configGroups[i].rows) { + /* jshint -W027 */ + if(configGroups[i].rows.length === 1) { // single-entry group + throw new Error("Grouping error: Group {" + configGroups[i].rows[0] + "} is invalid. Cannot define single-entry groups."); + return false; + } else if(configGroups[i].rows.length === 0) { + throw new Error("Grouping error: Cannot define empty groups."); + return false; + } + + rows.push(configGroups[i].rows); + + for (var j = 0, rowsLength = rows.length; j < rowsLength; j++) { + if (areRangesOverlapping(configGroups[i].rows, rows[j])) { + + throw new Error("Grouping error: ranges {" + configGroups[i].rows[0] + ", " + configGroups[i].rows[1] + "} and {" + rows[j][0] + ", " + rows[j][1] + "} are overlapping."); + return false; + } + } + } else if (configGroups[i].cols) { + + if(configGroups[i].cols.length === 1) { // single-entry group + throw new Error("Grouping error: Group {" + configGroups[i].cols[0] + "} is invalid. Cannot define single-entry groups."); + return false; + } else if(configGroups[i].cols.length === 0) { + throw new Error("Grouping error: Cannot define empty groups."); + return false; + } + + cols.push(configGroups[i].cols); + + for (var j = 0, colsLength = cols.length; j < colsLength; j++) { + if (areRangesOverlapping(configGroups[i].cols, cols[j])) { + + throw new Error("Grouping error: ranges {" + configGroups[i].cols[0] + ", " + configGroups[i].cols[1] + "} and {" + cols[j][0] + ", " + cols[j][1] + "} are overlapping."); + return false; + } + } + } + } + + return true; + }, + afterGetRowHeaderRenderers: function (arr) { + Handsontable.Grouping.groupIndicatorsFactory(arr, 'vertical'); + }, + afterGetColumnHeaderRenderers: function (arr) { + Handsontable.Grouping.groupIndicatorsFactory(arr, 'horizontal'); + }, + hookProxy: function (fn, arg) { + return function () { + if (instance.getSettings().groups) { + return arg ? Handsontable.Grouping[fn](arg).apply(this, arguments) : Handsontable.Grouping[fn].apply(this, arguments); + } else { + return void 0; + } + }; + } + }; +}; + +/** + * create new instance + */ +var init = function () { + var instance = this, + groupingSetting = !!(instance.getSettings().groups); + + + if (groupingSetting) { + var headerUpdates = {}; + + Handsontable.Grouping = new Grouping(instance); + + if (!instance.getSettings().rowHeaders) { // force using rowHeaders -- needs to be changed later + headerUpdates.rowHeaders = true; + } + if (!instance.getSettings().colHeaders) { // force using colHeaders -- needs to be changed later + headerUpdates.colHeaders = true; + } + if (headerUpdates.colHeaders || headerUpdates.rowHeaders) { + instance.updateSettings(headerUpdates); + } + + var groupConfigValid = Handsontable.Grouping.validateGroups(); + if (!groupConfigValid) { + return; + } + + instance.addHook('beforeInit', Handsontable.Grouping.hookProxy('init')); + instance.addHook('afterUpdateSettings', Handsontable.Grouping.hookProxy('updateGroups')); + instance.addHook('afterGetColumnHeaderRenderers', Handsontable.Grouping.hookProxy('afterGetColumnHeaderRenderers')); + instance.addHook('afterGetRowHeaderRenderers', Handsontable.Grouping.hookProxy('afterGetRowHeaderRenderers')); + instance.addHook('afterGetRowHeader', Handsontable.Grouping.hookProxy('afterGetRowHeader')); + instance.addHook('afterGetColHeader', Handsontable.Grouping.hookProxy('afterGetColHeader')); + instance.addHook('beforeOnCellMouseDown', Handsontable.Grouping.hookProxy('toggleGroupVisibility')); + instance.addHook('modifyTransformStart', Handsontable.Grouping.hookProxy('modifySelectionFactory', 'start')); + instance.addHook('modifyTransformEnd', Handsontable.Grouping.hookProxy('modifySelectionFactory', 'end')); + instance.addHook('modifyRowHeight', Handsontable.Grouping.hookProxy('modifyRowHeight')); + } +}; + +/** + * Update headers widths for the group indicators + */ +// TODO: this needs cleaning up +var updateHeaderWidths = function () { + var colgroups = document.querySelectorAll('colgroup'); + for (var i = 0, colgroupsLength = colgroups.length; i < colgroupsLength; i++) { + var rowHeaders = colgroups[i].querySelectorAll('col.rowHeader'); + if (rowHeaders.length === 0) { + return; + } + for (var j = 0, rowHeadersLength = rowHeaders.length + 1; j < rowHeadersLength; j++) { + if (rowHeadersLength == 2) { + return; + } + if (j < Handsontable.Grouping.getLevels().rows + 1) { + if (j == Handsontable.Grouping.getLevels().rows) { + Handsontable.Dom.addClass(rowHeaders[j], 'htGroupColClosest'); + } else { + Handsontable.Dom.addClass(rowHeaders[j], 'htGroupCol'); + } + } + } + } +}; + +Handsontable.hooks.add('beforeInit', init); + +Handsontable.hooks.add('afterUpdateSettings', function () { + + if (this.getSettings().groups && !Handsontable.Grouping) { + init.call(this, arguments); + } else if (!this.getSettings().groups && Handsontable.Grouping) { + Handsontable.Grouping.resetGroups(); + Handsontable.Grouping = void 0; + } +}); + +Handsontable.plugins.Grouping = Grouping; + +(function (Handsontable) { + /** + * Plugin used to allow user to copy and paste from the context menu + * Currently uses ZeroClipboard due to browser limitations + * @constructor + */ + function ContextMenuCopyPaste() { + this.zeroClipboardInstance = null; + this.instance = null; + } + + /** + * Configure ZeroClipboard + */ + ContextMenuCopyPaste.prototype.prepareZeroClipboard = function () { + if(this.swfPath) { + ZeroClipboard.config({ + swfPath: this.swfPath + }); + } + }; + + /** + * Copy action + * @returns {CopyPasteClass.elTextarea.value|*} + */ + ContextMenuCopyPaste.prototype.copy = function () { + this.instance.copyPaste.setCopyableText(); + return this.instance.copyPaste.copyPasteInstance.elTextarea.value; + }; + + /** + * Adds copy/paste items to context menu + */ + ContextMenuCopyPaste.prototype.addToContextMenu = function (defaultOptions) { + if (!this.getSettings().contextMenuCopyPaste) { + return; + } + + defaultOptions.items.unshift( + { + key: 'copy', + name: 'Copy' + }, + { + key: 'paste', + name: 'Paste', + callback: function () { + this.copyPaste.triggerPaste(); + } + }, + Handsontable.ContextMenu.SEPARATOR + ); + }; + + /** + * Setup ZeroClipboard swf clip position and event handlers + * @param cmInstance Current context menu instance + */ + ContextMenuCopyPaste.prototype.setupZeroClipboard = function (cmInstance) { + var plugin = this; + this.cmInstance = cmInstance; + + if (!Handsontable.Dom.hasClass(this.cmInstance.rootElement, 'htContextMenu')) { + return; + } + + var data = cmInstance.getData(); + for (var i = 0, ilen = data.length; i < ilen; i++) { //find position of 'copy' option + /*jshint -W083 */ + if (data[i].key === 'copy') { + this.zeroClipboardInstance = new ZeroClipboard(cmInstance.getCell(i, 0)); + + this.zeroClipboardInstance.off(); + this.zeroClipboardInstance.on("copy", function (event) { + var clipboard = event.clipboardData; + clipboard.setData("text/plain", plugin.copy()); + plugin.instance.getSettings().outsideClickDeselects = plugin.outsideClickDeselectsCache; + }); + + cmCopyPaste.bindEvents(); + break; + } + } + }; + + /** + * Bind all the standard events + */ + ContextMenuCopyPaste.prototype.bindEvents = function () { + var plugin = this; + + // Workaround for 'current' and 'zeroclipboard-is-hover' classes being stuck when moving the cursor over the context menu + if (plugin.cmInstance) { + + var eventManager = new Handsontable.eventManager(this.instance); + + var removeCurrenClass = function (event) { + var hadClass = plugin.cmInstance.rootElement.querySelector('td.current'); + if (hadClass) { + Handsontable.Dom.removeClass(hadClass, 'current'); + } + plugin.outsideClickDeselectsCache = plugin.instance.getSettings().outsideClickDeselects; + plugin.instance.getSettings().outsideClickDeselects = false; + }; + + var removeZeroClipboardClass = function (event) { + var hadClass = plugin.cmInstance.rootElement.querySelector('td.zeroclipboard-is-hover'); + if (hadClass) { + Handsontable.Dom.removeClass(hadClass, 'zeroclipboard-is-hover'); + } + plugin.instance.getSettings().outsideClickDeselects = plugin.outsideClickDeselectsCache; + }; + + eventManager.removeEventListener(document,'mouseenter', function () { + removeCurrenClass(); + }); + eventManager.addEventListener(document, 'mouseenter', function (e) { + removeCurrenClass(); + }); + + eventManager.removeEventListener(document,'mouseleave', function () { + removeZeroClipboardClass(); + }); + eventManager.addEventListener(document, 'mouseleave', function (e) { + removeZeroClipboardClass(); + }); + + + } + }; + + /** + * Initialize plugin + * @returns {boolean} Returns false if ZeroClipboard is not properly included + */ + ContextMenuCopyPaste.prototype.init = function () { + if (!this.getSettings().contextMenuCopyPaste) { + return; + } else if (typeof this.getSettings().contextMenuCopyPaste == "object") { + cmCopyPaste.swfPath = this.getSettings().contextMenuCopyPaste.swfPath; + } + + /* jshint ignore:start */ + if (typeof ZeroClipboard === 'undefined') { + throw new Error("To be able to use the Copy/Paste feature from the context menu, you need to manualy include ZeroClipboard.js file to your website."); + + return false; + } + try { + var flashTest = new ActiveXObject('ShockwaveFlash.ShockwaveFlash'); + } catch(exception) { + if (!('undefined' != typeof navigator.mimeTypes['application/x-shockwave-flash'])) { + throw new Error("To be able to use the Copy/Paste feature from the context menu, your browser needs to have Flash Plugin installed."); + + return false; + } + } + /* jshint ignore:end */ + + cmCopyPaste.instance = this; + cmCopyPaste.prepareZeroClipboard(); + }; + + var cmCopyPaste = new ContextMenuCopyPaste(); + + Handsontable.hooks.add('afterRender', function () { + cmCopyPaste.setupZeroClipboard(this); + }); + + Handsontable.hooks.add('afterInit', cmCopyPaste.init); + Handsontable.hooks.add('afterContextMenuDefaultOptions', cmCopyPaste.addToContextMenu); + Handsontable.ContextMenuCopyPaste = ContextMenuCopyPaste; + +})(Handsontable); + +(function (Handsontable) { + 'use strict'; + + function MultipleSelectionHandles(instance) { + this.instance = instance; + this.dragged = []; + + this.eventManager = Handsontable.eventManager(instance); + + this.bindTouchEvents(); + } + + MultipleSelectionHandles.prototype.getCurrentRangeCoords = function (selectedRange, currentTouch, touchStartDirection, currentDirection, draggedHandle) { + var topLeftCorner = selectedRange.getTopLeftCorner() + , bottomRightCorner = selectedRange.getBottomRightCorner() + , bottomLeftCorner = selectedRange.getBottomLeftCorner() + , topRightCorner = selectedRange.getTopRightCorner(); + + var newCoords = { + start: null, + end: null + }; + + switch (touchStartDirection) { + case "NE-SW": + switch (currentDirection) { + case "NE-SW": + case "NW-SE": + if (draggedHandle == "topLeft") { + newCoords = { + start: new WalkontableCellCoords(currentTouch.row, selectedRange.highlight.col), + end: new WalkontableCellCoords(bottomLeftCorner.row, currentTouch.col) + }; + } else { + newCoords = { + start: new WalkontableCellCoords(selectedRange.highlight.row, currentTouch.col), + end: new WalkontableCellCoords(currentTouch.row, topLeftCorner.col) + }; + } + break; + case "SE-NW": + if (draggedHandle == "bottomRight") { + newCoords = { + start: new WalkontableCellCoords(bottomRightCorner.row, currentTouch.col), + end: new WalkontableCellCoords(currentTouch.row, topLeftCorner.col) + }; + } + break; + //case "SW-NE": + // break; + } + break; + case "NW-SE": + switch (currentDirection) { + case "NE-SW": + if (draggedHandle == "topLeft") { + newCoords = { + start: currentTouch, + end: bottomLeftCorner + }; + } else { + newCoords.end = currentTouch; + } + break; + case "NW-SE": + if (draggedHandle == "topLeft") { + newCoords = { + start: currentTouch, + end: bottomRightCorner + }; + } else { + newCoords.end = currentTouch; + } + break; + case "SE-NW": + if (draggedHandle == "topLeft") { + newCoords = { + start: currentTouch, + end: topLeftCorner + }; + } else { + newCoords.end = currentTouch; + } + break; + case "SW-NE": + if (draggedHandle == "topLeft") { + newCoords = { + start: currentTouch, + end: topRightCorner + }; + } else { + newCoords.end = currentTouch; + } + break; + } + break; + case "SW-NE": + switch (currentDirection) { + case "NW-SE": + if (draggedHandle == "bottomRight") { + newCoords = { + start: new WalkontableCellCoords(currentTouch.row, topLeftCorner.col), + end: new WalkontableCellCoords(bottomLeftCorner.row, currentTouch.col) + }; + } else { + newCoords = { + start: new WalkontableCellCoords(topLeftCorner.row, currentTouch.col), + end: new WalkontableCellCoords(currentTouch.row, bottomRightCorner.col) + }; + } + break; + //case "NE-SW": + // + // break; + case "SW-NE": + if (draggedHandle == "topLeft") { + newCoords = { + start: new WalkontableCellCoords(selectedRange.highlight.row, currentTouch.col), + end: new WalkontableCellCoords(currentTouch.row, bottomRightCorner.col) + }; + } else { + newCoords = { + start: new WalkontableCellCoords(currentTouch.row, topLeftCorner.col), + end: new WalkontableCellCoords(topLeftCorner.row, currentTouch.col) + }; + } + break; + case "SE-NW": + if (draggedHandle == "bottomRight") { + newCoords = { + start: new WalkontableCellCoords(currentTouch.row, topRightCorner.col), + end: new WalkontableCellCoords(topLeftCorner.row, currentTouch.col) + }; + } else if (draggedHandle == "topLeft") { + newCoords = { + start: bottomLeftCorner, + end: currentTouch + }; + } + break; + } + break; + case "SE-NW": + switch (currentDirection) { + case "NW-SE": + case "NE-SW": + case "SW-NE": + if (draggedHandle == "topLeft") { + newCoords.end = currentTouch; + } + break; + case "SE-NW": + if (draggedHandle == "topLeft") { + newCoords.end = currentTouch; + } else { + newCoords = { + start: currentTouch, + end: topLeftCorner + }; + } + break; + } + break; + } + + return newCoords; + }; + + MultipleSelectionHandles.prototype.bindTouchEvents = function () { + var that = this; + var removeFromDragged = function (query) { + + if (this.dragged.length == 1) { + this.dragged = []; + return true; + } + + var entryPosition = this.dragged.indexOf(query); + + if (entryPosition == -1) { + return false; + } else if (entryPosition === 0) { + this.dragged = this.dragged.slice(0, 1); + } else if (entryPosition == 1) { + this.dragged = this.dragged.slice(-1); + } + }; + + this.eventManager.addEventListener(this.instance.rootElement,'touchstart', function (event) { + if(Handsontable.Dom.hasClass(event.target, "topLeftSelectionHandle-HitArea")) { + that.dragged.push("topLeft"); + var selectedRange = that.instance.getSelectedRange(); + that.touchStartRange = { + width: selectedRange.getWidth(), + height: selectedRange.getHeight(), + direction: selectedRange.getDirection() + }; + event.preventDefault(); + + return false; + } else if (Handsontable.Dom.hasClass(event.target, "bottomRightSelectionHandle-HitArea")) { + that.dragged.push("bottomRight"); + var selectedRange = that.instance.getSelectedRange(); + that.touchStartRange = { + width: selectedRange.getWidth(), + height: selectedRange.getHeight(), + direction: selectedRange.getDirection() + }; + event.preventDefault(); + + return false; + } + }); + + this.eventManager.addEventListener(this.instance.rootElement,'touchend', function (event) { + if(Handsontable.Dom.hasClass(event.target, "topLeftSelectionHandle-HitArea")) { + removeFromDragged.call(that, "topLeft"); + that.touchStartRange = void 0; + event.preventDefault(); + + return false; + } else if (Handsontable.Dom.hasClass(event.target, "bottomRightSelectionHandle-HitArea")) { + removeFromDragged.call(that, "bottomRight"); + that.touchStartRange = void 0; + event.preventDefault(); + + return false; + } + }); + + this.eventManager.addEventListener(this.instance.rootElement,'touchmove', function (event) { + var scrollTop = Handsontable.Dom.getWindowScrollTop() + , scrollLeft = Handsontable.Dom.getWindowScrollLeft(); + + if (that.dragged.length > 0) { + var endTarget = document.elementFromPoint( + event.touches[0].screenX - scrollLeft, + event.touches[0].screenY - scrollTop + ); + + if(!endTarget) { + return; + } + + if (endTarget.nodeName == "TD" || endTarget.nodeName == "TH") { + var targetCoords = that.instance.getCoords(endTarget); + + if(targetCoords.col == -1) { + targetCoords.col = 0; + } + + var selectedRange = that.instance.getSelectedRange() + , rangeWidth = selectedRange.getWidth() + , rangeHeight = selectedRange.getHeight() + , rangeDirection = selectedRange.getDirection(); + + if (rangeWidth == 1 && rangeHeight == 1) { + that.instance.selection.setRangeEnd(targetCoords); + } + + var newRangeCoords = that.getCurrentRangeCoords(selectedRange, targetCoords, that.touchStartRange.direction, rangeDirection, that.dragged[0]); + + if(newRangeCoords.start != null) { + that.instance.selection.setRangeStart(newRangeCoords.start); + } + that.instance.selection.setRangeEnd(newRangeCoords.end); + + } + + event.preventDefault(); + } + }); + + }; + + MultipleSelectionHandles.prototype.isDragged = function () { + if (this.dragged.length === 0) { + return false; + } else { + return true; + } + }; + + var init = function () { + var instance = this; + + Handsontable.plugins.multipleSelectionHandles = new MultipleSelectionHandles(instance); + }; + + Handsontable.hooks.add('afterInit', init); + +})(Handsontable); + +var TouchScroll = (function(instance) { + + function TouchScroll(instance) {} + + TouchScroll.prototype.init = function(instance) { + this.instance = instance; + this.bindEvents(); + + this.scrollbars = [ + this.instance.view.wt.wtScrollbars.vertical, + this.instance.view.wt.wtScrollbars.horizontal, + this.instance.view.wt.wtScrollbars.corner + ]; + + this.clones = [ + this.instance.view.wt.wtScrollbars.vertical.clone.wtTable.holder.parentNode, + this.instance.view.wt.wtScrollbars.horizontal.clone.wtTable.holder.parentNode, + this.instance.view.wt.wtScrollbars.corner.clone.wtTable.holder.parentNode + ]; + }; + + TouchScroll.prototype.bindEvents = function () { + var that = this; + + this.instance.addHook('beforeTouchScroll', function () { + Handsontable.freezeOverlays = true; + + for(var i = 0, cloneCount = that.clones.length; i < cloneCount ; i++) { + Handsontable.Dom.addClass(that.clones[i], 'hide-tween'); + } + }); + + this.instance.addHook('afterMomentumScroll', function () { + Handsontable.freezeOverlays = false; + + for(var i = 0, cloneCount = that.clones.length; i < cloneCount ; i++) { + Handsontable.Dom.removeClass(that.clones[i], 'hide-tween'); + } + + for(var i = 0, cloneCount = that.clones.length; i < cloneCount ; i++) { + Handsontable.Dom.addClass(that.clones[i], 'show-tween'); + } + + setTimeout(function () { + for(var i = 0, cloneCount = that.clones.length; i < cloneCount ; i++) { + Handsontable.Dom.removeClass(that.clones[i], 'show-tween'); + } + },400); + + for(var i = 0, cloneCount = that.scrollbars.length; i < cloneCount ; i++) { + that.scrollbars[i].refresh(); + that.scrollbars[i].resetFixedPosition(); + } + + }); + + }; + + return TouchScroll; +}()); + +var touchScrollHandler = new TouchScroll(); + +Handsontable.hooks.add('afterInit', function() { + touchScrollHandler.init.call(touchScrollHandler, this); +}); + +(function (Handsontable) { + function ManualColumnFreeze(instance) { + var fixedColumnsCount = instance.getSettings().fixedColumnsLeft; + + var init = function () { + // update plugin usages count for manualColumnPositions + if (typeof instance.manualColumnPositionsPluginUsages != 'undefined') { + instance.manualColumnPositionsPluginUsages.push('manualColumnFreeze'); + } else { + instance.manualColumnPositionsPluginUsages = ['manualColumnFreeze']; + } + + bindHooks(); + }; + + /** + * Modifies the default Context Menu entry list to consist 'freeze/unfreeze this column' entries + * @param {Object} defaultOptions + */ + function addContextMenuEntry(defaultOptions) { + defaultOptions.items.push( + Handsontable.ContextMenu.SEPARATOR, + { + key: 'freeze_column', + name: function () { + var selectedColumn = instance.getSelected()[1]; + if (selectedColumn > fixedColumnsCount - 1) { + return 'Freeze this column'; + } else { + return 'Unfreeze this column'; + } + }, + disabled: function () { + var selection = instance.getSelected(); + return selection[1] !== selection[3]; + }, + callback: function () { + var selectedColumn = instance.getSelected()[1]; + if (selectedColumn > fixedColumnsCount - 1) { + freezeColumn(selectedColumn); + } else { + unfreezeColumn(selectedColumn); + } + } + } + ); + } + + /** + * Increments the fixed columns count by one + */ + function addFixedColumn() { + instance.updateSettings({ + fixedColumnsLeft: fixedColumnsCount + 1 + }); + fixedColumnsCount++; + } + + /** + * Decrements the fixed columns count by one + */ + function removeFixedColumn() { + instance.updateSettings({ + fixedColumnsLeft: fixedColumnsCount - 1 + }); + fixedColumnsCount--; + } + + /** + * Checks whether 'manualColumnPositions' array needs creating and/or initializing + * @param {Number} [col] + */ + function checkPositionData(col) { + if (!instance.manualColumnPositions || instance.manualColumnPositions.length === 0) { + if (!instance.manualColumnPositions) { + instance.manualColumnPositions = []; + } + } + if (col) { + if (!instance.manualColumnPositions[col]) { + createPositionData(col + 1); + } + } else { + createPositionData(instance.countCols()); + } + } + + /** + * Fills the 'manualColumnPositions' array with consecutive column indexes + * @param {Number} len + */ + function createPositionData(len) { + if (instance.manualColumnPositions.length < len) { + for (var i = instance.manualColumnPositions.length; i < len; i++) { + instance.manualColumnPositions[i] = i; + } + } + } + + /** + * Updates the column order array used by modifyCol callback + * @param {Number} col + * @param {Number} actualCol column index of the currently selected cell + * @param {Number|null} returnCol suggested return slot for the unfreezed column (can be null) + * @param {String} action 'freeze' or 'unfreeze' + */ + function modifyColumnOrder(col, actualCol, returnCol, action) { + if (returnCol == null) { + returnCol = col; + } + + if (action === 'freeze') { + instance.manualColumnPositions.splice(fixedColumnsCount, 0, instance.manualColumnPositions.splice(actualCol, 1)[0]); + } else if (action === 'unfreeze') { + instance.manualColumnPositions.splice(returnCol, 0, instance.manualColumnPositions.splice(actualCol, 1)[0]); + } + } + + /** + * Estimates the most fitting return position for unfreezed column + * @param {Number} col + */ + function getBestColumnReturnPosition(col) { + var i = fixedColumnsCount, + j = getModifiedColumnIndex(i), + initialCol = getModifiedColumnIndex(col); + while (j < initialCol) { + i++; + j = getModifiedColumnIndex(i); + } + return i - 1; + } + + /** + * Freeze the given column (add it to fixed columns) + * @param {Number} col + */ + function freezeColumn(col) { + if (col <= fixedColumnsCount - 1) { + return; // already fixed + } + + var modifiedColumn = getModifiedColumnIndex(col) || col; + checkPositionData(modifiedColumn); + modifyColumnOrder(modifiedColumn, col, null, 'freeze'); + + addFixedColumn(); + } + + /** + * Unfreeze the given column (remove it from fixed columns and bring to it's previous position) + * @param {Number} col + */ + function unfreezeColumn(col) { + if (col > fixedColumnsCount - 1) { + return; // not fixed + } + + var returnCol = getBestColumnReturnPosition(col); + + var modifiedColumn = getModifiedColumnIndex(col) || col; + checkPositionData(modifiedColumn); + modifyColumnOrder(modifiedColumn, col, returnCol, 'unfreeze'); + removeFixedColumn(); + } + + function getModifiedColumnIndex(col) { + return instance.manualColumnPositions[col]; + } + + /** + * 'modiftyCol' callback + * @param {Number} col + */ + function onModifyCol(col) { + if (this.manualColumnPositionsPluginUsages.length > 1) { // if another plugin is using manualColumnPositions to modify column order, do not double the translation + return col; + } + return getModifiedColumnIndex(col); + } + + function bindHooks() { + //instance.addHook('afterGetColHeader', onAfterGetColHeader); + instance.addHook('modifyCol', onModifyCol); + instance.addHook('afterContextMenuDefaultOptions', addContextMenuEntry); + } + + return { + init: init, + freezeColumn: freezeColumn, + unfreezeColumn: unfreezeColumn, + helpers: { + addFixedColumn: addFixedColumn, + removeFixedColumn: removeFixedColumn, + checkPositionData: checkPositionData, + modifyColumnOrder: modifyColumnOrder, + getBestColumnReturnPosition: getBestColumnReturnPosition + } + }; + } + + var init = function init() { + if (!this.getSettings().manualColumnFreeze) { + return; + } + + var mcfPlugin; + + Handsontable.plugins.manualColumnFreeze = ManualColumnFreeze; + this.manualColumnFreeze = new ManualColumnFreeze(this); + + mcfPlugin = this.manualColumnFreeze; + mcfPlugin.init.call(this); + }; + + Handsontable.hooks.add('beforeInit', init); + +})(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); +}; + +WalkontableOverlay.prototype.makeClone = function (direction) { + var clone = document.createElement('DIV'); + clone.className = 'ht_clone_' + direction + ' handsontable'; + clone.style.position = 'absolute'; + clone.style.top = 0; + clone.style.left = 0; + 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.refresh = function (fastDraw) { + if (this.clone) { + this.clone.draw(fastDraw); + } +}; + +WalkontableOverlay.prototype.destroy = function () { + var eventManager = Handsontable.eventManager(this.clone); + + eventManager.clear(); +}; + +function WalkontableBorder(instance, settings) { + var style; + var createMultipleSelectorHandles = function () { + this.selectionHandles = { + topLeft: document.createElement('DIV'), + topLeftHitArea: document.createElement('DIV'), + bottomRight: document.createElement('DIV'), + bottomRightHitArea: document.createElement('DIV') + }; + var width = 10 + , hitAreaWidth = 40; + + this.selectionHandles.topLeft.className = 'topLeftSelectionHandle'; + this.selectionHandles.topLeftHitArea.className = 'topLeftSelectionHandle-HitArea'; + this.selectionHandles.bottomRight.className = 'bottomRightSelectionHandle'; + this.selectionHandles.bottomRightHitArea.className = 'bottomRightSelectionHandle-HitArea'; + + this.selectionHandles.styles = { + topLeft: this.selectionHandles.topLeft.style, + topLeftHitArea: this.selectionHandles.topLeftHitArea.style, + bottomRight: this.selectionHandles.bottomRight.style, + bottomRightHitArea: this.selectionHandles.bottomRightHitArea.style + }; + + var hitAreaStyle = { + 'position': 'absolute', + 'height': hitAreaWidth + 'px', + 'width': hitAreaWidth + 'px', + 'border-radius': parseInt(hitAreaWidth/1.5,10) + 'px' + }; + + for (var prop in hitAreaStyle) { + if (hitAreaStyle.hasOwnProperty(prop)) { + this.selectionHandles.styles.bottomRightHitArea[prop] = hitAreaStyle[prop]; + this.selectionHandles.styles.topLeftHitArea[prop] = hitAreaStyle[prop]; + } + } + + var handleStyle = { + 'position': 'absolute', + 'height': width + 'px', + 'width': width + 'px', + 'border-radius': parseInt(width/1.5,10) + 'px', + 'background': '#F5F5FF', + 'border': '1px solid #4285c8' + }; + + for (var prop in handleStyle) { + if (handleStyle.hasOwnProperty(prop)) { + this.selectionHandles.styles.bottomRight[prop] = handleStyle[prop]; + this.selectionHandles.styles.topLeft[prop] = handleStyle[prop]; + } + } + + this.main.appendChild(this.selectionHandles.topLeft); + this.main.appendChild(this.selectionHandles.bottomRight); + this.main.appendChild(this.selectionHandles.topLeftHitArea); + this.main.appendChild(this.selectionHandles.bottomRightHitArea); + }; + + if(!settings){ + return; + } + + var eventManager = Handsontable.eventManager(instance); + + //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.cornerDefaultStyle = { + width: '5px', + height: '5px', + borderWidth: '2px', + borderStyle: 'solid', + borderColor: '#FFF' + }; + + this.corner = this.main.childNodes[4]; + this.corner.className += ' corner'; + this.cornerStyle = this.corner.style; + this.cornerStyle.width = this.cornerDefaultStyle.width; + this.cornerStyle.height = this.cornerDefaultStyle.height; + this.cornerStyle.border = [ + this.cornerDefaultStyle.borderWidth, + this.cornerDefaultStyle.borderStyle, + this.cornerDefaultStyle.borderColor + ].join(' '); + + if(Handsontable.mobileBrowser) { + createMultipleSelectorHandles.call(this); + } + + 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; + + + + eventManager.addEventListener(document.body, 'mousedown', function () { + down = true; + }); + + + eventManager.addEventListener(document.body, 'mouseup', function () { + down = false; + }); + + /* jshint ignore:start */ + for (var c = 0, len = this.main.childNodes.length; c < len; c++) { + + eventManager.addEventListener(this.main.childNodes[c], 'mouseenter', function (event) { + if (!down || !instance.getSetting('hideBorderOnMouseDownOver')) { + return; + } + event.preventDefault(); + event.stopImmediatePropagation(); + + var bounds = this.getBoundingClientRect(); + + this.style.display = 'none'; + + 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; + } + }; + + var handler = function (event) { + if (isOutside(event)) { + eventManager.removeEventListener(document.body, 'mousemove', handler); + this.style.display = 'block'; + } + }; + eventManager.addEventListener(document.body, 'mousemove', handler); + }); + } + /* jshint ignore:end */ +} + +/** + * 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; + + var isPartRange = function () { + if(this.instance.selections.area.cellRange) { + + if (toRow != this.instance.selections.area.cellRange.to.row || + toColumn != this.instance.selections.area.cellRange.to.col) { + return true; + } + } + + return false; + }; + + var updateMultipleSelectionHandlesPosition = function (top, left, width, height) { + var handleWidth = parseInt(this.selectionHandles.styles.topLeft.width, 10) + , hitAreaWidth = parseInt(this.selectionHandles.styles.topLeftHitArea.width, 10); + + this.selectionHandles.styles.topLeft.top = parseInt(top - handleWidth,10) + "px"; + this.selectionHandles.styles.topLeft.left = parseInt(left - handleWidth,10) + "px"; + + this.selectionHandles.styles.topLeftHitArea.top = parseInt(top - (hitAreaWidth/4)*3,10) + "px"; + this.selectionHandles.styles.topLeftHitArea.left = parseInt(left - (hitAreaWidth/4)*3,10) + "px"; + + this.selectionHandles.styles.bottomRight.top = parseInt(top + height,10) + "px"; + this.selectionHandles.styles.bottomRight.left = parseInt(left + width,10) + "px"; + + this.selectionHandles.styles.bottomRightHitArea.top = parseInt(top + height - hitAreaWidth/4,10) + "px"; + this.selectionHandles.styles.bottomRightHitArea.left = parseInt(left + width - hitAreaWidth/4,10) + "px"; + + if(this.settings.border.multipleSelectionHandlesVisible && this.settings.border.multipleSelectionHandlesVisible()) { + this.selectionHandles.styles.topLeft.display = "block"; + this.selectionHandles.styles.topLeftHitArea.display = "block"; + if(!isPartRange.call(this)) { + this.selectionHandles.styles.bottomRight.display = "block"; + this.selectionHandles.styles.bottomRightHitArea.display = "block"; + } else { + this.selectionHandles.styles.bottomRight.display = "none"; + this.selectionHandles.styles.bottomRightHitArea.display = "none"; + } + } else { + this.selectionHandles.styles.topLeft.display = "none"; + this.selectionHandles.styles.bottomRight.display = "none"; + this.selectionHandles.styles.topLeftHitArea.display = "none"; + this.selectionHandles.styles.bottomRightHitArea.display = "none"; + } + + if(fromRow == this.instance.wtSettings.getSetting('fixedRowsTop') || fromColumn == this.instance.wtSettings.getSetting('fixedColumnsLeft')) { + this.selectionHandles.styles.topLeft.zIndex = "9999"; + this.selectionHandles.styles.topLeftHitArea.zIndex = "9999"; + } else { + this.selectionHandles.styles.topLeft.zIndex = ""; + this.selectionHandles.styles.topLeftHitArea.zIndex = ""; + } + + }; + + if (instance.cloneOverlay instanceof WalkontableVerticalScrollbarNative || instance.cloneOverlay instanceof WalkontableCornerScrollbarNative) { + ilen = instance.getSetting('fixedRowsTop'); + } + else { + ilen = instance.wtTable.getRenderedRowsCount(); + } + + for (i = 0; i < ilen; i++) { + s = instance.wtTable.rowFilter.renderedToSource(i); + if (s >= corners[0] && s <= corners[2]) { + fromRow = s; + break; + } + } + + for (i = ilen - 1; i >= 0; i--) { + s = instance.wtTable.rowFilter.renderedToSource(i); + if (s >= corners[0] && s <= corners[2]) { + toRow = s; + break; + } + } + + ilen = instance.wtTable.getRenderedColumnsCount(); + + for (i = 0; i < ilen; i++) { + s = instance.wtTable.columnFilter.renderedToSource(i); + if (s >= corners[1] && s <= corners[3]) { + fromColumn = s; + break; + } + } + + for (i = ilen - 1; i >= 0; i--) { + s = instance.wtTable.columnFilter.renderedToSource(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 (Handsontable.mobileBrowser || (!this.hasSetting(this.settings.border.cornerVisible) || isPartRange.call(this))) { + this.cornerStyle.display = 'none'; + } + else { + this.cornerStyle.top = top + height - 4 + 'px'; + this.cornerStyle.left = left + width - 4 + 'px'; + this.cornerStyle.borderRightWidth = this.cornerDefaultStyle.borderWidth; + this.cornerStyle.width = this.cornerDefaultStyle.width; + this.cornerStyle.display = 'block'; + + if (!instance.cloneOverlay && toColumn === instance.wtTable.getRenderedColumnsCount() - 1) { + var scrollableElement = Handsontable.Dom.getScrollableElement(instance.wtTable.TABLE), + needShrinkCorner = toTD.offsetLeft + Handsontable.Dom.outerWidth(toTD) >= Handsontable.Dom.innerWidth(scrollableElement); + + if (needShrinkCorner) { + this.cornerStyle.borderRightWidth = '0px'; + this.cornerStyle.width = Math.ceil(parseInt(this.cornerDefaultStyle.width, 10) / 2) + 'px'; + } + } + } + + if (Handsontable.mobileBrowser) { + updateMultipleSelectionHandlesPosition.call(this, top, left, width, height); + } +}; + +/** + * 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'; + + if(Handsontable.mobileBrowser) { + this.selectionHandles.styles.topLeft.display = 'none'; + this.selectionHandles.styles.bottomRight.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; +}; + +WalkontableCellCoords.prototype.isSouthWestOf = function (testedCoords) { + return this.row >= testedCoords.row && this.col <= testedCoords.col; +}; + +WalkontableCellCoords.prototype.isNorthEastOf = 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 selected range height (in number of rows) + * @returns {number} + */ +WalkontableCellRange.prototype.getHeight = function () { + return Math.max(this.from.row, this.to.row) - Math.min(this.from.row, this.to.row) + 1; +}; + +/** + * Returns selected range width (in number of columns) + * @returns {number} + */ +WalkontableCellRange.prototype.getWidth = function () { + return Math.max(this.from.col, this.to.col) - Math.min(this.from.col, this.to.col) + 1; +}; + +/** + * 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(); + + if (cellCoords.row < 0) { + cellCoords.row = 0; + } + + if (cellCoords.col < 0) { + cellCoords.col = 0; + } + + 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()); +}; + +WalkontableCellRange.prototype.isEqual = function (testedRange) { + return (Math.min(this.from.row, this.to.row) == Math.min(testedRange.from.row, testedRange.to.row)) && + (Math.max(this.from.row, this.to.row) == Math.max(testedRange.from.row, testedRange.to.row)) && + (Math.min(this.from.col, this.to.col) == Math.min(testedRange.from.col, testedRange.to.col)) && + (Math.max(this.from.col, this.to.col) == Math.max(testedRange.from.col, testedRange.to.col)); +}; + +/** + * 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() + , bottomRight = this.getBottomRightCorner() + , topRight = this.getTopRightCorner() + , bottomLeft = this.getBottomLeftCorner(); + + 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); + + var finalFrom = new WalkontableCellCoords(resultTopRow, resultTopCol) + , finalTo = new WalkontableCellCoords(resultBottomRow, resultBottomCol); + var isCorner = new WalkontableCellRange(finalFrom, finalFrom, finalTo).isCorner(this.from, expandingRange) + , onlyMerge = expandingRange.isEqual(new WalkontableCellRange(finalFrom, finalFrom, finalTo)); + + if (isCorner && !onlyMerge) { + if (this.from.col > finalFrom.col) { + finalFrom.col = resultBottomCol; + finalTo.col = resultTopCol; + } + if (this.from.row > finalFrom.row) { + finalFrom.row = resultBottomRow; + finalTo.row = resultTopRow; + } + } + + this.from = finalFrom; + this.to = finalTo; + + return true; +}; + +WalkontableCellRange.prototype.getDirection = function () { + if (this.from.isNorthWestOf(this.to)) { // NorthWest - SouthEast + return "NW-SE"; + } else if (this.from.isNorthEastOf(this.to)) { // NorthEast - SouthWest + return "NE-SW"; + } else if (this.from.isSouthEastOf(this.to)) { // SouthEast - NorthWest + return "SE-NW"; + } else if (this.from.isSouthWestOf(this.to)) { // SouthWest - NorthEast + return "SW-NE"; + } +}; + +WalkontableCellRange.prototype.setDirection = function (direction) { + switch (direction) { + case "NW-SE" : + this.from = this.getTopLeftCorner(); + this.to = this.getBottomRightCorner(); + break; + case "NE-SW" : + this.from = this.getTopRightCorner(); + this.to = this.getBottomLeftCorner(); + break; + case "SE-NW" : + this.from = this.getBottomRightCorner(); + this.to = this.getTopLeftCorner(); + break; + case "SW-NE" : + this.from = this.getBottomLeftCorner(); + this.to = this.getTopRightCorner(); + break; + } +}; + +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.getTopRightCorner = function () { + return new WalkontableCellCoords(Math.min(this.from.row, this.to.row), Math.max(this.from.col, this.to.col)); +}; + +WalkontableCellRange.prototype.getBottomLeftCorner = function () { + return new WalkontableCellCoords(Math.max(this.from.row, this.to.row), Math.min(this.from.col, this.to.col)); +}; + +WalkontableCellRange.prototype.isCorner = function (coords, expandedRange) { + if (expandedRange) { + if (expandedRange.includes(coords)) { + if (this.getTopLeftCorner().isEqual(new WalkontableCellCoords(expandedRange.from.row, expandedRange.from.col)) || + this.getTopRightCorner().isEqual(new WalkontableCellCoords(expandedRange.from.row, expandedRange.to.col)) || + this.getBottomLeftCorner().isEqual(new WalkontableCellCoords(expandedRange.to.row, expandedRange.from.col)) || + this.getBottomRightCorner().isEqual(new WalkontableCellCoords(expandedRange.to.row, expandedRange.to.col))) { + return true; + } + } + } + return coords.isEqual(this.getTopLeftCorner()) || coords.isEqual(this.getTopRightCorner()) || coords.isEqual(this.getBottomLeftCorner()) || coords.isEqual(this.getBottomRightCorner()); +}; + +WalkontableCellRange.prototype.getOppositeCorner = function (coords, expandedRange) { + if (!(coords instanceof WalkontableCellCoords)) { + return false; + } + + if (expandedRange) { + if (expandedRange.includes(coords)) { + if (this.getTopLeftCorner().isEqual(new WalkontableCellCoords(expandedRange.from.row, expandedRange.from.col))) { + return this.getBottomRightCorner(); + } + if (this.getTopRightCorner().isEqual(new WalkontableCellCoords(expandedRange.from.row, expandedRange.to.col))) { + return this.getBottomLeftCorner(); + } + if (this.getBottomLeftCorner().isEqual(new WalkontableCellCoords(expandedRange.to.row, expandedRange.from.col))) { + return this.getTopRightCorner(); + } + if (this.getBottomRightCorner().isEqual(new WalkontableCellCoords(expandedRange.to.row, expandedRange.to.col))) { + return this.getTopLeftCorner(); + } + } + } + + if (coords.isEqual(this.getBottomRightCorner())) { + return this.getTopLeftCorner(); + } else if (coords.isEqual(this.getTopLeftCorner())) { + return this.getBottomRightCorner(); + } else if (coords.isEqual(this.getTopRightCorner())) { + return this.getBottomLeftCorner(); + } else if (coords.isEqual(this.getBottomLeftCorner())) { + return this.getTopRightCorner(); + } +}; + +WalkontableCellRange.prototype.getBordersSharedWith = function (range) { + if (!this.includesRange(range)) { + return []; + } + + var thisBorders = { + top: Math.min(this.from.row, this.to.row), + bottom: Math.max(this.from.row, this.to.row), + left: Math.min(this.from.col, this.to.col), + right: Math.max(this.from.col, this.to.col) + } + , rangeBorders = { + top: Math.min(range.from.row, range.to.row), + bottom: Math.max(range.from.row, range.to.row), + left: Math.min(range.from.col, range.to.col), + right: Math.max(range.from.col, range.to.col) + } + , result = []; + + if (thisBorders.top == rangeBorders.top) { + result.push('top'); + } + if (thisBorders.right == rangeBorders.right) { + result.push('right'); + } + if (thisBorders.bottom == rangeBorders.bottom) { + result.push('bottom'); + } + if (thisBorders.left == rangeBorders.left) { + result.push('left'); + } + + return result; +}; + +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 + +/** + * WalkontableColumnFilter + * @constructor + */ +function WalkontableColumnFilter(offset,total, countTH) { + this.offset = offset; + this.total = total; + this.countTH = countTH; +} + +WalkontableColumnFilter.prototype.offsetted = function (n) { + return n + this.offset; +}; + +WalkontableColumnFilter.prototype.unOffsetted = function (n) { + return n - this.offset; +}; + +WalkontableColumnFilter.prototype.renderedToSource = function (n) { + return this.offsetted(n); +}; + +WalkontableColumnFilter.prototype.sourceToRendered = function (n) { + return this.unOffsetted(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.renderedToSource(this.offsettedTH(n)); +}; + +WalkontableColumnFilter.prototype.sourceColumnToVisibleRowHeadedColumn = function (n) { + return this.unOffsettedTH(this.sourceToRendered(n)); +}; + +/** + * WalkontableColumnStrategy + * @param containerSizeFn + * @param sizeAtIndex + * @param strategy - all, last, none + * @constructor + */ +function WalkontableColumnStrategy(instance, containerSizeFn, sizeAtIndex, strategy) { + var size + , i = 0; + + this.instance = instance; + 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.visibleCellCount++; + } + this.cellSizes.push(size); + this.cellSizesSum += size; + this.cellCount++; + + i++; + } + + var containerSize = this.getContainerSize(); + 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.getContainerSize = function () { + return typeof this.containerSizeFn === 'function' ? this.containerSizeFn() : this.containerSizeFn; +}; + +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() + , 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 && 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.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); + this.selections = this.getSetting('selections'); + + 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; +} + +/** + * Force rerender of Walkontable + * @param fastDraw {Boolean} When TRUE, try to refresh only the positions of borders without rerendering the data. + * It will only work if WalkontableTable.draw() does not force rendering anyway + * @returns {Walkontable} + */ +Walkontable.prototype.draw = function (fastDraw) { + this.drawInterrupted = false; + if (!fastDraw && !Handsontable.Dom.isVisible(this.wtTable.TABLE)) { + this.drawInterrupted = true; //draw interrupted because TABLE is not visible + return; + } + + this.wtTable.draw(fastDraw); + + return this; +}; + +/** + * Returns the TD at coords. If topmost is set to true, returns TD from the topmost overlay layer, + * if not set or set to false, returns TD from the master table. + * @param {WalkontableCellCoords} coords + * @param {Boolean} topmost + * @returns {Object} + */ +Walkontable.prototype.getCell = function (coords, topmost) { + if(!topmost) { + return this.wtTable.getCell(coords); + } else { + var fixedRows = this.wtSettings.getSetting('fixedRowsTop') + , fixedColumns = this.wtSettings.getSetting('fixedColumnsLeft'); + + if(coords.row < fixedRows && coords.col < fixedColumns) { + return this.wtScrollbars.corner.clone.wtTable.getCell(coords); + } else if(coords.row < fixedRows) { + return this.wtScrollbars.vertical.clone.wtTable.getCell(coords); + } else if (coords.col < fixedColumns) { + return this.wtScrollbars.horizontal.clone.wtTable.getCell(coords); + } else { + return this.wtTable.getCell(coords); + } + } +}; + +Walkontable.prototype.update = function (settings, value) { + return this.wtSettings.update(settings, value); +}; + +/** + * Scroll the viewport to a row at the given index in the data source + * @param row + * @returns {Walkontable} + */ +Walkontable.prototype.scrollVertical = function (row) { + this.wtScrollbars.vertical.scrollTo(row); + this.getSetting('onScrollVertically'); + return this; +}; + +/** + * Scroll the viewport to a column at the given index in the data source + * @param row + * @returns {Walkontable} + */ +Walkontable.prototype.scrollHorizontal = function (column) { + this.wtScrollbars.horizontal.scrollTo(column); + this.getSetting('onScrollHorizontally'); + return this; +}; + +/** + * 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/handsontable/wiki/JavaScript-&-DOM-performance-tips +}; + +Walkontable.prototype.hasSetting = function (key) { + return this.wtSettings.has(key); +}; + +Walkontable.prototype.destroy = function () { + this.wtScrollbars.destroy(); + + if ( 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; + + Handsontable.Dom.addClass(this.clone.wtTable.holder.parentNode, 'wtDebugVisible'); + + /*var that = this; + var lastX = 0; + var lastY = 0; + var overlayContainer = that.clone.wtTable.holder.parentNode; + + var eventManager = Handsontable.eventManager(instance); + + eventManager.addEventListener(document.body, 'mousemove', 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.destroy = function () { + WalkontableOverlay.prototype.destroy.call(this); + clearTimeout(this.lastTimeout); +}; + +function WalkontableEvent(instance) { + var that = this; + + var eventManager = Handsontable.eventManager(instance); + + //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 onTouchMove = function (event) { + that.instance.touchMoving = true; + }; + + var longTouchTimeout; + + ///** + // * Update touch event target - if user taps on resize handle 'hit area', update the target to the cell itself + // * @param event + // */ + /* + var adjustTapTarget = function (event) { + var currentSelection + , properTarget; + + if(Handsontable.Dom.hasClass(event.target,'SelectionHandle')) { + if(that.instance.selections[0].cellRange) { + currentSelection = that.instance.selections[0].cellRange.highlight; + + properTarget = that.instance.getCell(currentSelection, true); + } + } + + if(properTarget) { + Object.defineProperty(event,'target',{ + value: properTarget + }); + } + + return event; + };*/ + + var onTouchStart = function (event) { + var container = this; + + eventManager.addEventListener(this, 'touchmove', onTouchMove); + + //this.addEventListener("touchmove", onTouchMove, false); + + // touch-and-hold event + //longTouchTimeout = setTimeout(function () { + // if(!that.instance.touchMoving) { + // that.instance.longTouch = true; + // + // var targetCoords = Handsontable.Dom.offset(event.target); + // var contextMenuEvent = new MouseEvent('contextmenu', { + // clientX: targetCoords.left + event.target.offsetWidth, + // clientY: targetCoords.top + event.target.offsetHeight, + // button: 2 + // }); + // + // that.instance.wtTable.holder.parentNode.parentNode.dispatchEvent(contextMenuEvent); + // } + //},200); + + // Prevent cell selection when scrolling with touch event - not the best solution performance-wise + that.checkIfTouchMove = setTimeout(function () { + if (that.instance.touchMoving === true) { + that.instance.touchMoving = void 0; + + eventManager.removeEventListener("touchmove", onTouchMove, false); + + return; + } else { + //event = adjustTapTarget(event); + + onMouseDown(event); + } + }, 30); + + //eventManager.removeEventListener(that.instance.wtTable.holder, "mousedown", onMouseDown); + }; + + 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); + } + } + }; + + + var onTouchEnd = function (event) { + clearTimeout(longTouchTimeout); + //that.instance.longTouch == void 0; + + event.preventDefault(); + + onMouseUp(event); + + //eventManager.removeEventListener(that.instance.wtTable.holder, "mouseup", onMouseUp); + }; + + eventManager.addEventListener(this.instance.wtTable.holder, 'mousedown', onMouseDown); + + eventManager.addEventListener(this.instance.wtTable.TABLE, 'mouseover', onMouseOver); + + eventManager.addEventListener(this.instance.wtTable.holder, 'mouseup', onMouseUp); + + + if(this.instance.wtTable.holder.parentNode.parentNode && Handsontable.mobileBrowser) { // check if full HOT instance, or detached WOT AND run on mobile device + var classSelector = "." + this.instance.wtTable.holder.parentNode.className.split(" ").join("."); + + eventManager.addEventListener(this.instance.wtTable.holder.parentNode.parentNode, 'touchstart', function (event) { + that.instance.touchApplied = true; + if (Handsontable.Dom.isChildOf(event.target, classSelector)) { + onTouchStart.call(event.target, event); + } + }); + eventManager.addEventListener(this.instance.wtTable.holder.parentNode.parentNode, 'touchend', function (event) { + that.instance.touchApplied = false; + if (Handsontable.Dom.isChildOf(event.target, classSelector)) { + onTouchEnd.call(event.target, event); + } + }); + + if(!that.instance.momentumScrolling) { + that.instance.momentumScrolling = {}; + } + eventManager.addEventListener(this.instance.wtTable.holder.parentNode.parentNode, 'scroll', function (event) { + clearTimeout(that.instance.momentumScrolling._timeout); + + if(!that.instance.momentumScrolling.ongoing) { + that.instance.getSetting('onBeforeTouchScroll'); + } + that.instance.momentumScrolling.ongoing = true; + + that.instance.momentumScrolling._timeout = setTimeout(function () { + if(!that.instance.touchApplied) { + that.instance.momentumScrolling.ongoing = false; + + that.instance.getSetting('onAfterMomentumScroll'); + } + },200); + }); + } + + eventManager.addEventListener(window, 'resize', function () { + that.instance.draw(); + }); + + this.destroy = function () { + clearTimeout(this.dblClickTimeout[0]); + clearTimeout(this.dblClickTimeout[1]); + + eventManager.clear(); + }; +} + +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; //selections.current is current selected cell + cell.TD = this.instance.wtTable.getCell(cell.coords); + } else if (Handsontable.Dom.hasClass(elem, 'wtBorder') && Handsontable.Dom.hasClass(elem, 'area')) { + if (this.instance.selections.area.cellRange) { + cell.coords = this.instance.selections.area.cellRange.to; //selections.area is area selected cells + 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; +} + +/** + * 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; + /* jshint -W121 */ + 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.renderedToSource = function (n) { + return this.offsetted(n); +}; + +WalkontableRowFilter.prototype.sourceToRendered = function (n) { + return this.unOffsetted(n); +}; + +WalkontableRowFilter.prototype.offsettedTH = function (n) { + return n - this.countTH; +}; + +WalkontableRowFilter.prototype.visibleColHeadedRowToSourceRow = function (n) { + return this.renderedToSource(this.offsettedTH(n)); +}; + +WalkontableRowFilter.prototype.sourceRowToVisibleColHeadedRow = function (n) { + return this.unOffsettedTH(this.sourceToRendered(n)); +}; + +function WalkontableScroll(instance) { + this.instance = instance; +} + +/** + * Scrolls viewport to a cell by minimum number of cells + * @param {WalkontableCellCoords} coords + */ +WalkontableScroll.prototype.scrollViewport = function (coords) { + if (!this.instance.drawn) { + return; + } + + var 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'); + } + + if (coords.row > this.instance.wtTable.getLastVisibleRow()) { + this.instance.wtScrollbars.vertical.scrollTo(coords.row, true); + } else if (coords.row >= this.instance.getSetting('fixedRowsTop') && coords.row < this.instance.wtTable.getFirstVisibleRow()){ + this.instance.wtScrollbars.vertical.scrollTo(coords.row); + } + + if (coords.col > this.instance.wtTable.getLastVisibleColumn()) { + this.instance.wtScrollbars.horizontal.scrollTo(coords.col, true); + } else if (coords.col > this.instance.getSetting('fixedColumnsLeft') && coords.col < this.instance.wtTable.getFirstVisibleColumn()){ + this.instance.wtScrollbars.horizontal.scrollTo(coords.col); + } + + //} +}; + +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, + finalLeft, + finalTop; + + if (this.scrollHandler === window) { + var box = this.instance.wtTable.holder.getBoundingClientRect(); + var top = Math.ceil(box.top); + var left = Math.ceil(box.left); + var bottom = Math.ceil(box.bottom); + var right = Math.ceil(box.right); + + if (left < 0 && (right - elem.offsetWidth) > 0) { + finalLeft = -left + 'px'; + } else { + finalLeft = '0'; + } + + if (top < 0 && (bottom - elem.offsetHeight) > 0) { + finalTop = -top + "px"; + } else { + finalTop = "0"; + } + } + else if(!Handsontable.freezeOverlays) { + finalLeft = this.instance.wtScrollbars.horizontal.getScrollPosition() + "px"; + finalTop = this.instance.wtScrollbars.vertical.getScrollPosition() + "px"; + } + + Handsontable.Dom.setOverlayPosition(elem, finalLeft, finalTop); + + elem.style.width = Handsontable.Dom.outerWidth(this.clone.wtTable.TABLE) + 4 + 'px'; + elem.style.height = Handsontable.Dom.outerHeight(this.clone.wtTable.TABLE) + 4 + 'px'; +}; + +function WalkontableHorizontalScrollbarNative(instance) { + this.instance = instance; + this.type = 'horizontal'; + 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 () { + var finalLeft, finalTop; + + 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); + var right = Math.ceil(box.right); + + if (left < 0 && (right - elem.offsetWidth) > 0) { + finalLeft = -left + 'px'; + } else { + finalLeft = '0'; + } + + finalTop = this.instance.wtTable.hider.style.top; + } + else if(!Handsontable.freezeOverlays) { + finalLeft = this.getScrollPosition() + "px"; + finalTop = this.instance.wtTable.hider.style.top; + } + + Handsontable.Dom.setOverlayPosition(elem, finalLeft, finalTop); + + elem.style.height = Handsontable.Dom.outerHeight(this.clone.wtTable.TABLE) + 'px'; + elem.style.width = Handsontable.Dom.outerWidth(this.clone.wtTable.TABLE) + 4 + 'px';// + 4 + 'px'; +}; + +WalkontableHorizontalScrollbarNative.prototype.refresh = function (fastDraw) { + this.applyToDOM(); + WalkontableOverlay.prototype.refresh.call(this, fastDraw); +}; + +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 () { + this.instance.getSetting('onScrollHorizontally'); +}; + +WalkontableHorizontalScrollbarNative.prototype.sumCellSizes = function (from, length) { + var sum = 0; + while(from < length) { + sum += this.instance.wtTable.getStretchedColumnWidth(from) || this.instance.wtSettings.defaultColumnWidth; + from++; + } + return sum; +}; + +//applyToDOM (in future merge it with this.refresh?) +WalkontableHorizontalScrollbarNative.prototype.applyToDOM = function () { + var total = this.instance.getSetting('totalColumns'); + var headerSize = this.instance.wtViewport.getRowHeaderWidth(); + + this.fixedContainer.style.width = headerSize + this.sumCellSizes(0, total) + 'px';// + 4 + 'px'; + + if (typeof this.instance.wtViewport.columnsRenderCalculator.startPosition === 'number'){ + this.fixed.style.left = this.instance.wtViewport.columnsRenderCalculator.startPosition + 'px'; + } + else if (total === 0) { + this.fixed.style.left = '0'; + } else { + throw new Error('Incorrect value of the columnsRenderCalculator'); + } + this.fixed.style.right = ''; +}; + +/** + * Scrolls horizontally to a column at the left edge of the viewport + * @param sourceCol {Number} + * @param beyondRendered {Boolean} if TRUE, scrolls according to the bottom edge (top edge is by default) + */ +WalkontableHorizontalScrollbarNative.prototype.scrollTo = function (sourceCol, beyondRendered) { + var newX = this.getTableParentOffset(); + + if (beyondRendered) { + newX += this.sumCellSizes(0, sourceCol + 1); + newX -= this.instance.wtViewport.getViewportWidth(); + } + else { + var fixedColumnsLeft = this.instance.getSetting('fixedColumnsLeft'); + newX += this.sumCellSizes(fixedColumnsLeft, sourceCol); + } + + this.setScrollPosition(newX); +}; + +WalkontableHorizontalScrollbarNative.prototype.getTableParentOffset = function () { + if (this.scrollHandler === window) { + return this.instance.wtTable.holderOffset.left; + } + else { + return 0; + } +}; + +function WalkontableVerticalScrollbarNative(instance) { + this.instance = instance; + this.type = 'vertical'; + this.init(); + this.clone = this.makeClone('top'); +} + +WalkontableVerticalScrollbarNative.prototype = new WalkontableOverlay(); + +//resetFixedPosition (in future merge it with this.refresh?) +WalkontableVerticalScrollbarNative.prototype.resetFixedPosition = function () { + var finalLeft, finalTop; + + 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 bottom = Math.ceil(box.bottom); + + finalLeft = this.instance.wtTable.hider.style.left; + + if (top < 0 && (bottom - elem.offsetHeight) > 0) { + finalTop = -top + "px"; + } else { + finalTop = "0"; + } + } + else if(!Handsontable.freezeOverlays) { + finalTop = this.getScrollPosition() + "px"; + finalLeft = this.instance.wtTable.hider.style.left; + } + + Handsontable.Dom.setOverlayPosition(elem, finalLeft, finalTop); + + 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';// + 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 () { + this.instance.getSetting('onScrollVertically'); +}; + +WalkontableVerticalScrollbarNative.prototype.sumCellSizes = function (from, length) { + var sum = 0; + while (from < length) { + sum += this.instance.wtTable.getRowHeight(from) || this.instance.wtSettings.settings.defaultRowHeight; //TODO optimize getSetting, because this is MUCH faster then getSetting + from++; + } + return sum; +}; + +WalkontableVerticalScrollbarNative.prototype.refresh = function (fastDraw) { + this.applyToDOM(); + WalkontableOverlay.prototype.refresh.call(this, fastDraw); +}; + +//applyToDOM (in future merge it with this.refresh?) +WalkontableVerticalScrollbarNative.prototype.applyToDOM = function () { + var total = this.instance.getSetting('totalRows'); + var headerSize = this.instance.wtViewport.getColumnHeaderHeight(); + + this.fixedContainer.style.height = headerSize + this.sumCellSizes(0, total) + 'px'; + if (typeof this.instance.wtViewport.rowsRenderCalculator.startPosition === 'number') { + this.fixed.style.top = this.instance.wtViewport.rowsRenderCalculator.startPosition + 'px'; + } + else if (total === 0) { + this.fixed.style.top = '0'; //can happen if there are 0 rows + } + else { + throw new Error("Incorrect value of the rowsRenderCalculator"); + } + this.fixed.style.bottom = ''; +}; + +/** + * Scrolls vertically to a row + * + * @param sourceRow {Number} + * @param bottomEdge {Boolean} if TRUE, scrolls according to the bottom edge (top edge is by default) + */ +WalkontableVerticalScrollbarNative.prototype.scrollTo = function (sourceRow, bottomEdge) { + var newY = this.getTableParentOffset(); + + if (bottomEdge) { + newY += this.sumCellSizes(0, sourceRow + 1); + newY -= this.instance.wtViewport.getViewportHeight(); + // Fix 1 pixel offset when cell is selected + newY += 1; + } + else { + var fixedRowsTop = this.instance.getSetting('fixedRowsTop'); + newY += this.sumCellSizes(fixedRowsTop, sourceRow); + } + + this.setScrollPosition(newY); +}; + +WalkontableVerticalScrollbarNative.prototype.getTableParentOffset = function () { + if (this.scrollHandler === window) { + return this.instance.wtTable.holderOffset.top; + } + else { + return 0; + } +}; + +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; + + this.refreshAll = 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.instance.draw(true); + that.vertical.onScroll(); + that.horizontal.onScroll(); + }; + + var eventManager = Handsontable.eventManager(that.instance); + + eventManager.addEventListener(this.vertical.scrollHandler, 'scroll', this.refreshAll); + if (this.vertical.scrollHandler !== this.horizontal.scrollHandler) { + eventManager.addEventListener(this.horizontal.scrollHandler, 'scroll', this.refreshAll); + } + + if (this.vertical.scrollHandler !== window && this.horizontal.scrollHandler !== window) { + eventManager.addEventListener(window,'scroll', this.refreshAll); + } +}; + +WalkontableScrollbars.prototype.destroy = function () { + var eventManager = Handsontable.eventManager(this.instance); + + if (this.vertical) { + this.vertical.destroy(); + eventManager.removeEventListener(this.vertical.scrollHandler,'scroll', this.refreshAll); + } + if (this.horizontal) { + this.horizontal.destroy(); + eventManager.removeEventListener(this.horizontal.scrollHandler,'scroll', this.refreshAll); + } + eventManager.removeEventListener(window,'scroll', this.refreshAll); + if (this.corner ) { + this.corner.destroy(); + } + if (this.debug) { + this.debug.destroy(); + } +}; + +WalkontableScrollbars.prototype.refresh = function (fastDraw) { + if (this.horizontal) { + this.horizontal.refresh(fastDraw); + } + if (this.vertical) { + this.vertical.refresh(fastDraw); + } + if (this.corner) { + this.corner.refresh(fastDraw); + } + if (this.debug) { + this.debug.refresh(fastDraw); + } +}; + +WalkontableScrollbars.prototype.applyToDOM = function () { + if (this.horizontal) { + this.horizontal.applyToDOM(); + } + if (this.vertical) { + this.vertical.applyToDOM(); + } +}; + +function WalkontableSelection(settings, cellRange) { + this.settings = settings; + this.cellRange = cellRange || null; + this.instanceBorders = {}; +} + +/** + * Each Walkontable clone requires it's own border for every selection. This method creates and returns selection borders per instance + * @param {Walkontable} instance + * @returns {WalkontableBorder} + */ +WalkontableSelection.prototype.getBorder = function (instance) { + if (this.instanceBorders[instance.guid]) { + return this.instanceBorders[instance.guid]; + } + //where is this returned? + this.instanceBorders[instance.guid] = new WalkontableBorder(instance, this.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(), + bottomRight = this.cellRange.getBottomRightCorner(); + + return [topLeft.row, topLeft.col, bottomRight.row, bottomRight.col]; +}; + +WalkontableSelection.prototype.addClassAtCoords = function (instance, sourceRow, sourceColumn, cls) { + var TD = instance.wtTable.getCell(new WalkontableCellCoords(sourceRow, sourceColumn)); + + if (typeof TD === 'object') { + Handsontable.Dom.addClass(TD, cls); + } +}; + +WalkontableSelection.prototype.draw = function (instance) { + var + _this = this, + renderedRows = instance.wtTable.getRenderedRowsCount(), + renderedColumns = instance.wtTable.getRenderedColumnsCount(), + corners, sourceRow, sourceCol, border, TH; + + if (this.isEmpty()) { + if (this.settings.border) { + border = this.getBorder(instance); + + if (border) { + border.disappear(); + } + } + + return; + } + + corners = this.getCorners(); + + for (var column = 0; column < renderedColumns; column++) { + sourceCol = instance.wtTable.columnFilter.renderedToSource(column); + + if (sourceCol >= corners[1] && sourceCol <= corners[3]) { + TH = instance.wtTable.getColumnHeader(sourceCol); + + if (TH && _this.settings.highlightColumnClassName) { + Handsontable.Dom.addClass(TH, _this.settings.highlightColumnClassName); + } + } + } + + for (var row = 0; row < renderedRows; row++) { + sourceRow = instance.wtTable.rowFilter.renderedToSource(row); + + if (sourceRow >= corners[0] && sourceRow <= corners[2]) { + TH = instance.wtTable.getRowHeader(sourceRow); + + if (TH && _this.settings.highlightRowClassName) { + Handsontable.Dom.addClass(TH, _this.settings.highlightRowClassName); + } + } + + for (var column = 0; column < renderedColumns; column++) { + sourceCol = instance.wtTable.columnFilter.renderedToSource(column); + + if (sourceRow >= corners[0] && sourceRow <= corners[2] && sourceCol >= corners[1] && sourceCol <= corners[3]) { + // selected cell + if (_this.settings.className) { + _this.addClassAtCoords(instance, sourceRow, sourceCol, _this.settings.className); + } + } + else if (sourceRow >= corners[0] && sourceRow <= corners[2]) { + // selection is in this row + if (_this.settings.highlightRowClassName) { + _this.addClassAtCoords(instance, sourceRow, sourceCol, _this.settings.highlightRowClassName); + } + } + else if (sourceCol >= corners[1] && sourceCol <= corners[3]) { + // selection is in this column + if (_this.settings.highlightColumnClassName) { + _this.addClassAtCoords(instance, sourceRow, sourceCol, _this.settings.highlightColumnClassName); + } + } + } + } + + instance.getSetting('onBeforeDrawBorders', corners, this.settings.className); + + if (this.settings.border) { + border = this.getBorder(instance); + + if (border) { + // warning! border.appear modifies corners! + border.appear(corners); + } + } +}; + +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, + 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, + cellRenderer: function (row, column, TD) { + var cellData = that.getSetting('data', row, column); + Handsontable.Dom.fastInnerText(TD, cellData === void 0 || cellData === null ? '' : cellData); + }, + //columnWidth: 50, + columnWidth: function (col) { + return; //return undefined means use default size for the rendered cell content + }, + rowHeight: function (row) { + return; //return undefined means use default size for the rendered cell content + }, + defaultRowHeight: 23, + defaultColumnWidth: 50, + selections: null, + hideBorderOnMouseDownOver: false, + viewportRowCalculatorOverride: null, + viewportColumnCalculatorOverride: null, + + //callbacks + onCellMouseDown: null, + onCellMouseOver: null, +// onCellMouseOut: null, + onCellDblClick: null, + onCellCornerMouseDown: null, + onCellCornerDblClick: null, + beforeDraw: null, + onDraw: null, + onBeforeDrawBorders: null, + onScrollVertically: null, + onScrollHorizontally: null, + onBeforeTouchScroll: null, + onAfterMomentumScroll: null, + + //constants + scrollbarWidth: 10, + scrollbarHeight: 10, + + renderAllRows: false, + groups: 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/handsontable/wiki/JavaScript-&-DOM-performance-tips + } + else if (param1 !== void 0 && Array.isArray(this.settings[key])) { //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; + + if (!this.isWorkingOnClone()) { + this.holder.parentNode.style.position = "relative"; + } + + //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.rowFilter = null; + this.columnFilter = null; +} + +WalkontableTable.prototype.isWorkingOnClone = function () { + return !!this.instance.cloneSource; +}; + +/** + * Redraws the table + * @param fastDraw {Boolean} If TRUE, will try to avoid full redraw and only update the border positions. If FALSE or UNDEFINED, will perform a full redraw + * @returns {WalkontableTable} + */ +WalkontableTable.prototype.draw = function (fastDraw) { + if (!this.isWorkingOnClone()) { + this.holderOffset = Handsontable.Dom.offset(this.holder); + fastDraw = this.instance.wtViewport.createRenderCalculators(fastDraw); + } + + if (!fastDraw) { + if (this.isWorkingOnClone()) { + this.tableOffset = this.instance.cloneSource.wtTable.tableOffset; + } + else { + this.tableOffset = Handsontable.Dom.offset(this.TABLE); + } + var startRow; + if (this.instance.cloneOverlay instanceof WalkontableDebugOverlay || + this.instance.cloneOverlay instanceof WalkontableVerticalScrollbarNative || + this.instance.cloneOverlay instanceof WalkontableCornerScrollbarNative) { + startRow = 0; + } + else { + startRow = this.instance.wtViewport.rowsRenderCalculator.startRow; + } + + + var startColumn; + if (this.instance.cloneOverlay instanceof WalkontableDebugOverlay || + this.instance.cloneOverlay instanceof WalkontableHorizontalScrollbarNative || + this.instance.cloneOverlay instanceof WalkontableCornerScrollbarNative) { + startColumn = 0; + } else { + startColumn = this.instance.wtViewport.columnsRenderCalculator.startColumn; + } + + this.rowFilter = new WalkontableRowFilter( + startRow, + this.instance.getSetting('totalRows'), + this.instance.getSetting('columnHeaders').length + ); + this.columnFilter = new WalkontableColumnFilter( + startColumn, + this.instance.getSetting('totalColumns'), + this.instance.getSetting('rowHeaders').length + ); + this._doDraw(); //creates calculator after draw + } + else { + if (!this.isWorkingOnClone()) { + //in case we only scrolled without redraw, update visible rows information in oldRowsCalculator + this.instance.wtViewport.createVisibleCalculators(); + } + if (this.instance.wtScrollbars) { + this.instance.wtScrollbars.refresh(true); + } + } + + this.refreshSelections(fastDraw); + + if (!this.isWorkingOnClone()) { + this.instance.wtScrollbars.vertical.resetFixedPosition(); + this.instance.wtScrollbars.horizontal.resetFixedPosition(); + this.instance.wtScrollbars.corner.resetFixedPosition(); + } + + this.instance.drawn = true; + return this; +}; + +WalkontableTable.prototype._doDraw = function () { + var wtRenderer = new WalkontableTableRenderer(this); + wtRenderer.render(); +}; + +WalkontableTable.prototype.removeClassFromCells = function (className) { + var nodes = this.TABLE.querySelectorAll('.' + className); + for (var i = 0, ilen = nodes.length; i < ilen; i++) { + Handsontable.Dom.removeClass(nodes[i], className); + } +}; + +WalkontableTable.prototype.refreshSelections = function (fastDraw) { + var i, len; + + if (!this.instance.selections) { + return; + } + len = this.instance.selections.length; + + if (fastDraw) { + for (i = 0; i < len; i++) { + // there was no rerender, so we need to remove classNames by ourselves + if (this.instance.selections[i].settings.className) { + this.removeClassFromCells(this.instance.selections[i].settings.className); + } + if (this.instance.selections[i].settings.highlightRowClassName) { + this.removeClassFromCells(this.instance.selections[i].settings.highlightRowClassName); + } + if (this.instance.selections[i].settings.highlightColumnClassName) { + this.removeClassFromCells(this.instance.selections[i].settings.highlightColumnClassName); + } + } + } + for (i = 0; i < len; i++) { + this.instance.selections[i].draw(this.instance, fastDraw); + } +}; + +/** + * 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.isRowBeforeRenderedRows(coords.row)) { + return -1; //row before rendered rows + } + else if (this.isRowAfterRenderedRows(coords.row)) { + return -2; //row after rendered rows + } + + var TR = this.TBODY.childNodes[this.rowFilter.sourceToRendered(coords.row)]; + + if (TR) { + return TR.childNodes[this.columnFilter.sourceColumnToVisibleRowHeadedColumn(coords.col)]; + } +}; + +/** + * getColumnHeader + * @param col + * @param level Header level (0 = most distant to the table) + * @return {Object} HTMLElement on success or undefined on error + * + */ +WalkontableTable.prototype.getColumnHeader = function(col, level) { + if(!level) { + level = 0; + } + + var TR = this.THEAD.childNodes[level]; + if (TR) { + return TR.childNodes[this.columnFilter.sourceColumnToVisibleRowHeadedColumn(col)]; + } +}; + +/** + * getRowHeader + * @param row + * @return {Object} HTMLElement on success or {Number} one of the exit codes on error: + * null table doesn't have row headers + * + */ +WalkontableTable.prototype.getRowHeader = function(row) { + if(this.columnFilter.sourceColumnToVisibleRowHeadedColumn(0) === 0) { + return null; + } + + var TR = this.TBODY.childNodes[this.rowFilter.sourceToRendered(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.renderedToSource(row); + } + + return new WalkontableCellCoords( + row, + this.columnFilter.visibleRowHeadedColumnToSourceColumn(TD.cellIndex) + ); +}; + +WalkontableTable.prototype.getTrForRow = function (row) { + return this.TBODY.childNodes[this.rowFilter.sourceToRendered(row)]; +}; + +WalkontableTable.prototype.getFirstRenderedRow = function () { + return this.instance.wtViewport.rowsRenderCalculator.startRow; +}; + +WalkontableTable.prototype.getFirstVisibleRow = function () { + return this.instance.wtViewport.rowsVisibleCalculator.startRow; +}; + +WalkontableTable.prototype.getFirstRenderedColumn = function () { + return this.instance.wtViewport.columnsRenderCalculator.startColumn; +}; + +//returns -1 if no column is visible +WalkontableTable.prototype.getFirstVisibleColumn = function () { + return this.instance.wtViewport.columnsVisibleCalculator.startColumn; +}; + +//returns -1 if no row is visible +WalkontableTable.prototype.getLastRenderedRow = function () { + return this.instance.wtViewport.rowsRenderCalculator.endRow; +}; + +WalkontableTable.prototype.getLastVisibleRow = function () { + return this.instance.wtViewport.rowsVisibleCalculator.endRow; +}; + +WalkontableTable.prototype.getLastRenderedColumn = function () { + return this.instance.wtViewport.columnsRenderCalculator.endColumn; +}; + +//returns -1 if no column is visible +WalkontableTable.prototype.getLastVisibleColumn = function () { + return this.instance.wtViewport.columnsVisibleCalculator.endColumn; +}; + +WalkontableTable.prototype.isRowBeforeRenderedRows = function (r) { + return (this.rowFilter.sourceToRendered(r) < 0 && r >= 0); +}; + +WalkontableTable.prototype.isRowAfterViewport = function (r) { + return (r > this.getLastVisibleRow()); +}; + +WalkontableTable.prototype.isRowAfterRenderedRows = function (r) { + return (r > this.getLastRenderedRow()); +}; + +WalkontableTable.prototype.isColumnBeforeViewport = function (c) { + return (this.columnFilter.sourceToRendered(c) < 0 && c >= 0); +}; + +WalkontableTable.prototype.isColumnAfterViewport = function (c) { + return (c > this.getLastVisibleColumn()); +}; + +WalkontableTable.prototype.isLastRowFullyVisible = function () { + return (this.getLastVisibleRow() === this.getLastRenderedRow()); +}; + +WalkontableTable.prototype.isLastColumnFullyVisible = function () { + return (this.getLastVisibleColumn() === this.getLastRenderedColumn); +}; + +WalkontableTable.prototype.getRenderedColumnsCount = function () { + if (this.instance.cloneOverlay instanceof WalkontableDebugOverlay) { + return this.instance.getSetting('totalColumns'); + } + else if (this.instance.cloneOverlay instanceof WalkontableHorizontalScrollbarNative || this.instance.cloneOverlay instanceof WalkontableCornerScrollbarNative) { + return this.instance.getSetting('fixedColumnsLeft'); + } + else { + return this.instance.wtViewport.columnsRenderCalculator.count; + } +}; + +WalkontableTable.prototype.getRenderedRowsCount = function () { + if (this.instance.cloneOverlay instanceof WalkontableDebugOverlay) { + return this.instance.getSetting('totalRows'); + } + else if (this.instance.cloneOverlay instanceof WalkontableVerticalScrollbarNative || this.instance.cloneOverlay instanceof WalkontableCornerScrollbarNative) { + return this.instance.getSetting('fixedRowsTop'); + } + return this.instance.wtViewport.rowsRenderCalculator.count; +}; + +WalkontableTable.prototype.getVisibleRowsCount = function () { + return this.instance.wtViewport.rowsVisibleCalculator.count; +}; + +WalkontableTable.prototype.allRowsInViewport = function () { + return this.instance.getSetting('totalRows') == this.getVisibleRowsCount(); +}; + +/** + * Checks if any of the row's cells content exceeds its initial height, and if so, returns the oversized height + * @param {Number} sourceRow + * @return {Number} + */ +WalkontableTable.prototype.getRowHeight = function (sourceRow) { + var height = this.instance.wtSettings.settings.rowHeight(sourceRow); + var oversizedHeight = this.instance.wtViewport.oversizedRows[sourceRow]; + if (oversizedHeight !== void 0) { + height = height ? Math.max(height, oversizedHeight) : oversizedHeight; + } + return height; +}; + +WalkontableTable.prototype.getColumnHeaderHeight = function (level) { + var height = this.instance.wtSettings.settings.defaultRowHeight, + oversizedHeight = this.instance.wtViewport.oversizedColumnHeaders[level]; + if (oversizedHeight !== void 0) { + height = height ? Math.max(height, oversizedHeight) : oversizedHeight; + } + return height; +}; + +WalkontableTable.prototype.getVisibleColumnsCount = function () { + return this.instance.wtViewport.columnsVisibleCalculator.count; +}; + + +WalkontableTable.prototype.allColumnsInViewport = function () { + return this.instance.getSetting('totalColumns') == this.getVisibleColumnsCount(); +}; + + + +WalkontableTable.prototype.getColumnWidth = function (sourceColumn) { + var width = this.instance.wtSettings.settings.columnWidth; + if(typeof width === 'function') { + width = width(sourceColumn); + } else if(typeof width === 'object') { + width = width[sourceColumn]; + } + + var oversizedWidth = this.instance.wtViewport.oversizedCols[sourceColumn]; + if (oversizedWidth !== void 0) { + width = width ? Math.max(width, oversizedWidth) : oversizedWidth; + } + return width; +}; + +WalkontableTable.prototype.getStretchedColumnWidth = function (sourceColumn) { + var + width = this.getColumnWidth(sourceColumn) || this.instance.wtSettings.settings.defaultColumnWidth, + calculator = this.instance.wtViewport.columnsRenderCalculator, + stretchedWidth; + + if (calculator) { + stretchedWidth = calculator.getStretchedColumnWidth(sourceColumn, width); + + if (stretchedWidth) { + width = stretchedWidth; + } + } + + return width; +}; + + +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'); + this.columnHeaderCount = this.columnHeaders.length; + + var visibleColIndex + , totalRows = this.instance.getSetting('totalRows') + , totalColumns = this.instance.getSetting('totalColumns') + , displayTds + , adjusted = false + , workspaceWidth + , cloneLimit = this.wtTable.getRenderedRowsCount(); + + if (totalColumns > 0) { + this.adjustAvailableNodes(); + adjusted = true; + + this.renderColGroups(); + + this.renderColumnHeaders(); + + displayTds = this.wtTable.getRenderedColumnsCount(); + + //Render table rows + this.renderRows(totalRows, cloneLimit, displayTds); + + if (!this.wtTable.isWorkingOnClone()) { + workspaceWidth = this.instance.wtViewport.getWorkspaceWidth(); + this.instance.wtViewport.containerWidth = null; + } else { + this.adjustColumnHeaderHeights(); + } + + this.adjustColumnWidths(displayTds); + } + + if (!adjusted) { + this.adjustAvailableNodes(); + } + + this.removeRedundantRows(cloneLimit); + + if (!this.wtTable.isWorkingOnClone()) { + this.markOversizedRows(); + + this.instance.wtViewport.createVisibleCalculators(); + + 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; + + var firstRendered = this.wtTable.getFirstRenderedColumn(); + var lastRendered = this.wtTable.getLastRenderedColumn(); + + for (var i = firstRendered ; i < lastRendered; i++) { + var width = this.wtTable.getStretchedColumnWidth(i); + var renderedIndex = this.columnFilter.sourceToRendered(i); + this.COLGROUP.childNodes[renderedIndex + this.rowHeaderCount].style.width = width + 'px'; + } + } + + this.instance.wtScrollbars.refresh(false); + + this.instance.getSetting('onDraw', true); + } + +}; + +WalkontableTableRenderer.prototype.removeRedundantRows = function (renderedRowsCount) { + while (this.wtTable.tbodyChildrenLength > renderedRowsCount) { + this.TBODY.removeChild(this.TBODY.lastChild); + this.wtTable.tbodyChildrenLength--; + } +}; + +WalkontableTableRenderer.prototype.renderRows = function (totalRows, cloneLimit, displayTds) { + var lastTD, TR; + var visibleRowIndex = 0; + var sourceRowIndex = this.rowFilter.renderedToSource(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); + + //after last column is rendered, check if last cell is fully displayed + if (!isWorkingOnClone) { + this.resetOversizedRow(sourceRowIndex); + } + + + if (TR.firstChild) { + //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 + var height = this.instance.wtTable.getRowHeight(sourceRowIndex); + if (height) { + TR.firstChild.style.height = height + 'px'; + } + else { + TR.firstChild.style.height = ''; + } + } + + visibleRowIndex++; + + sourceRowIndex = this.rowFilter.renderedToSource(visibleRowIndex); + } +}; + +WalkontableTableRenderer.prototype.resetOversizedRow = function (sourceRow) { + if (this.instance.wtViewport.oversizedRows && this.instance.wtViewport.oversizedRows[sourceRow]) { + this.instance.wtViewport.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.renderedToSource(rowCount); + previousRowHeight = this.instance.wtTable.getRowHeight(sourceRowIndex); + currentTr = this.instance.wtTable.getTrForRow(sourceRowIndex); + + trInnerHeight = Handsontable.Dom.innerHeight(currentTr) - 1; + + if ((!previousRowHeight && this.instance.wtSettings.settings.defaultRowHeight < trInnerHeight || previousRowHeight < trInnerHeight)) { + this.instance.wtViewport.oversizedRows[sourceRowIndex] = trInnerHeight; + } + } + +}; + +WalkontableTableRenderer.prototype.adjustColumnHeaderHeights = function () { + var columnHeaders = this.instance.getSetting('columnHeaders'); + for(var i = 0, columnHeadersCount = columnHeaders.length; i < columnHeadersCount; i++) { + if(this.instance.wtViewport.oversizedColumnHeaders[i]) { + if(this.instance.wtTable.THEAD.childNodes[i].childNodes.length === 0) { + return; + } + this.instance.wtTable.THEAD.childNodes[i].childNodes[0].style.height = this.instance.wtViewport.oversizedColumnHeaders[i] + "px"; + } + } +}; + +WalkontableTableRenderer.prototype.markIfOversizedColumnHeader = function (col) { + var colCount = this.instance.wtTable.THEAD.childNodes.length !== 0 ? this.instance.wtTable.THEAD.childNodes[0].childNodes.length : 0, + sourceColIndex, + previousColHeaderHeight, + currentHeader, + currentHeaderHeight, + columnHeaders = this.instance.getSetting('columnHeaders'), + columnHeaderCount = columnHeaders.length, + level = columnHeaderCount; + + sourceColIndex = this.instance.wtTable.columnFilter.renderedToSource(col); + + while(level) { + level--; + + previousColHeaderHeight = this.instance.wtTable.getColumnHeaderHeight(level); + currentHeader = this.instance.wtTable.getColumnHeader(sourceColIndex, level); + + if(!currentHeader) { + continue; + } + + currentHeaderHeight = Handsontable.Dom.innerHeight(currentHeader) - 1; + + if ((!previousColHeaderHeight && this.instance.wtSettings.settings.defaultRowHeight < currentHeaderHeight || previousColHeaderHeight < currentHeaderHeight)) { + this.instance.wtViewport.oversizedColumnHeaders[level] = currentHeaderHeight; + } + } +}; + +WalkontableTableRenderer.prototype.renderCells = function (sourceRowIndex, TR, displayTds) { + var TD, sourceColIndex; + + for (var visibleColIndex = 0; visibleColIndex < displayTds; visibleColIndex++) { + sourceColIndex = this.columnFilter.renderedToSource(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); + } + + if (!Handsontable.Dom.hasClass(TD, 'hide')) { + TD.className = ''; + } + + TD.removeAttribute('style'); + + this.instance.wtSettings.settings.cellRenderer(sourceRowIndex, sourceColIndex, TD); + + } + + return TD; +}; + +WalkontableTableRenderer.prototype.adjustColumnWidths = function (displayTds) { + var width; + + this.instance.wtViewport.columnsRenderCalculator.refreshStretching(this.instance.wtViewport.getViewportWidth()); + + for (var renderedColIndex = 0; renderedColIndex < displayTds; renderedColIndex++) { + width = this.wtTable.getStretchedColumnWidth(this.columnFilter.renderedToSource(renderedColIndex)); + this.COLGROUP.childNodes[renderedColIndex + this.rowHeaderCount].style.width = width + 'px'; + } +}; + +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){ + TH.className = ''; + TH.removeAttribute('style'); + this.rowHeaders[col](row, TH, col); +}; + +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 () { + //adjust COLGROUP + this.adjustColGroups(); + + //adjust THEAD + this.adjustThead(); +}; + +WalkontableTableRenderer.prototype.renderColumnHeaders = function () { + if (!this.columnHeaderCount) { + return; + } + + var columnCount = this.wtTable.getRenderedColumnsCount(), + TR, + renderedColumnIndex; + + for (var i = 0; i < this.columnHeaderCount; i++) { + TR = this.getTrForColumnHeaders(i); + + for (renderedColumnIndex = (-1) * this.rowHeaderCount; renderedColumnIndex < columnCount; renderedColumnIndex++) { + var sourceCol = this.columnFilter.renderedToSource(renderedColumnIndex); + this.renderColumnHeader(i, sourceCol, TR.childNodes[renderedColumnIndex + this.rowHeaderCount]); + + if(!this.wtTable.isWorkingOnClone()) { + this.markIfOversizedColumnHeader(renderedColumnIndex); + } + } + } +}; + +WalkontableTableRenderer.prototype.adjustColGroups = function () { + var columnCount = this.wtTable.getRenderedColumnsCount(); + + //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--; + } +}; + +WalkontableTableRenderer.prototype.adjustThead = function () { + var columnCount = this.wtTable.getRenderedColumnsCount(); + var TR = this.THEAD.firstChild; + if (this.columnHeaders.length) { + + for (var i = 0, columnHeadersLength = this.columnHeaders.length; i < columnHeadersLength; i++) { + TR = this.THEAD.childNodes[i]; + 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--; + } + } + + var theadChildrenLength = this.THEAD.childNodes.length; + if(theadChildrenLength > this.columnHeaders.length) { + for(var i = this.columnHeaders.length; i < theadChildrenLength; i++ ) { + this.THEAD.removeChild(this.THEAD.lastChild); + } + } + } + + else if (TR) { + Handsontable.Dom.empty(TR); + } +}; + +WalkontableTableRenderer.prototype.getTrForColumnHeaders = function (index) { + var TR = this.THEAD.childNodes[index]; + return TR; +}; + +WalkontableTableRenderer.prototype.renderColumnHeader = function (row, col, TH) { + TH.className = ''; + TH.removeAttribute('style'); + return this.columnHeaders[row](col, TH, row); +}; + +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.removeRedundantColumns = function (renderedColumnsCount) { + while (this.wtTable.tbodyChildrenLength > renderedColumnsCount) { + this.TBODY.removeChild(this.TBODY.lastChild); + this.wtTable.tbodyChildrenLength--; + } +}; + +/* + 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.oversizedRows = []; + this.oversizedCols = []; + this.oversizedColumnHeaders = []; + + var that = this; + + var eventManager = Handsontable.eventManager(instance); + eventManager.addEventListener(window,'resize',function () { + that.clientHeight = that.getWorkspaceHeight(); + }); +} + +WalkontableViewport.prototype.getWorkspaceHeight = function () { + var scrollHandler = this.instance.wtScrollbars.vertical.scrollHandler; + if (scrollHandler === window) { + return document.documentElement.clientHeight; + } + else { + var elemHeight = Handsontable.Dom.outerHeight(scrollHandler); + var height = (elemHeight > 0 && scrollHandler.clientHeight > 0) ? scrollHandler.clientHeight : Infinity; //returns height without DIV scrollbar + return height; + } +}; + + +WalkontableViewport.prototype.getWorkspaceWidth = function () { + var width, + totalColumns = this.instance.getSetting("totalColumns"), + scrollHandler = this.instance.wtScrollbars.horizontal.scrollHandler, + overflow, + stretchSetting = this.instance.getSetting('stretchH'); + + if(Handsontable.freezeOverlays) { + width = Math.min(document.documentElement.offsetWidth - this.getWorkspaceOffset().left, document.documentElement.offsetWidth); + } else { + width = Math.min(this.getContainerFillWidth(), document.documentElement.offsetWidth - this.getWorkspaceOffset().left, document.documentElement.offsetWidth); + } + + if (scrollHandler === window && totalColumns > 0 && this.sumColumnWidths(0, totalColumns - 1) > width) { + //in case sum of column widths is higher than available stylesheet width, let's assume using the whole window + //otherwise continue below, which will allow stretching + //this is used in `scroll_window.html` + //TODO test me + return document.documentElement.clientWidth; + } + + if (scrollHandler !== window){ + overflow = this.instance.wtScrollbars.horizontal.scrollHandler.style.overflow; + + if (overflow == "scroll" || overflow == "hidden" || overflow == "auto") { + //this is used in `scroll.html` + //TODO test me + return Math.max(width, scrollHandler.clientWidth); + } + } + + if(stretchSetting === 'none' || !stretchSetting) { + // if no stretching is used, return the maximum used workspace width + return Math.max(width, Handsontable.Dom.outerWidth(this.instance.wtTable.TABLE)); + } else { + // if stretching is used, return the actual container width, so the columns can fit inside it + return width; + } +}; + +WalkontableViewport.prototype.sumColumnWidths = function (from, length) { + var sum = 0; + while(from < length) { + sum += this.instance.wtTable.getColumnWidth(from) || this.instance.wtSettings.defaultColumnWidth; + from++; + } + return sum; +}; +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 + * @param {Boolean} topmost + * @public + * @return {Element} + */ + this.getCell = function (row, col, topmost) { + return instance.view.getCellAtCoords(new WalkontableCellCoords(row, col), topmost); + }; + + /** + * Returns coordinates for the provided element + * @param elem + * @returns {WalkontableCellCoords|*} + */ + this.getCoords = function(elem) { + return this.view.wt.wtTable.getCoords.call(this.view.wt.wtTable, elem); + }; + + /** + * 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 = [], + range; + + range = datamap.getRange( + new WalkontableCellCoords(0, datamap.propToCol(prop)), + new WalkontableCellCoords(priv.settings.data.length - 1, datamap.propToCol(prop)), + datamap.DESTINATION_RENDERER); + + return out.concat.apply(out, range); + }; + + /** + * 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); + /* jshint ignore:start */ + if(cellMeta[key] != undefined){ + delete priv.cellSettings[row][col][key]; + } + /* jshint ignore:end */ + }; + + /** + * 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') { + /* jshint -W089 */ + for (var key in prop) { + var value = prop[key]; + 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.run(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){ + // warning: this must be done after datamap.colToProp + return Handsontable.hooks.run(instance, 'modifyCol', col); + } + + 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; + + /* jshint ignore:start */ + 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--; + } + /* jshint ignore:end */ + 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 (Array.isArray(priv.settings.rowHeaders) && 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.run(instance, 'modifyCol', col); + + if (priv.settings.columns && priv.settings.columns[col] && priv.settings.columns[col].title) { + return priv.settings.columns[col].title; + } + else if (Array.isArray(priv.settings.colHeaders) && 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.run(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); + + height = Handsontable.hooks.run(instance, 'modifyRowHeight', height, row); + + return height; + }; + + /** + * 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 rendered row + * @return {Number} + */ + this.rowOffset = function () { + return instance.view.wt.wtTable.getFirstRenderedRow(); + }; + + /** + * Return index of first visible column + * @return {Number} + */ + this.colOffset = function () { + return instance.view.wt.wtTable.getFirstRenderedColumn(); + }; + + /** + * Return number of rendered rows (including rows partially or fully rendered outside viewport). Returns -1 if table is not visible + * @return {Number} + */ + this.countRenderedRows = function () { + return instance.view.wt.drawn ? instance.view.wt.wtTable.getRenderedRowsCount() : -1; + }; + + /** + * Return number of visible rows (rendered rows that fully fit inside viewport)). Returns -1 if table is not visible + * @return {Number} + */ + this.countVisibleRows = function () { + return instance.view.wt.drawn ? instance.view.wt.wtTable.getVisibleRowsCount() : -1; + }; + + /** + * Return number of rendered columns (including columns partially or fully rendered outside viewport). Returns -1 if table is not visible + * @return {Number} + */ + this.countRenderedCols = function () { + return instance.view.wt.drawn ? instance.view.wt.wtTable.getRenderedColumnsCount() : -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.getVisibleColumnsCount() : - 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.run(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) { + /* jshint ignore:start */ + arguments[1] = datamap.propToCol(arguments[1]); + if (typeof arguments[3] !== "undefined") { + arguments[3] = datamap.propToCol(arguments[3]); + } + return instance.selectCell.apply(instance, arguments); + /* jshint ignore:end */ + }; + + /** + * 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.destroy(); + } + + + Handsontable.Dom.empty(instance.rootElement); + eventManager.clear(); + + 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") { + 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; + }; + + /** + * 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) { + return Handsontable.hooks.run(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) { + return elem; + } + elem = elem.parentNode; + } + return null; +}; + +/** + * Goes up the DOM tree and checks if element is child of another element + * @param child Child element + * @param {Object|string} parent Parent element OR selector of the parent element. If classname provided, function returns true for the first occurance of element with that class. + * @returns {boolean} + */ +Handsontable.Dom.isChildOf = function (child, parent) { + var node = child.parentNode; + var queriedParents = []; + if(typeof parent === "string") { + queriedParents = Array.prototype.slice.call(document.querySelectorAll(parent), 0); + } else { + queriedParents.push(parent); + } + + while (node != null) { + if (queriedParents.indexOf(node) > - 1) { + 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; + if (elem.previousSibling) { + /* jshint ignore:start */ + while (elem = elem.previousSibling) { + ++i; + } + /* jshint ignore:end */ + } + 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) { + if (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 + } + }; +} + +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; + /* jshint ignore:start */ + while (child = element.lastChild) { + element.removeChild(child); + } + /* jshint ignore:end */ +}; + +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 if element is attached to the DOM and visible, false otherwise + * @param elem + * @returns {boolean} + */ +Handsontable.Dom.isVisible = function (elem) { + 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. Function is not compatible with jQuery offset. + * + * @param {HTMLElement} elem + * @return {Object} Returns object with `top` and `left` props + */ +Handsontable.Dom.offset = function (elem) { + var offsetLeft, + offsetTop, + lastElem, + docElem, + box; + + docElem = document.documentElement; + + 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 + box = elem.getBoundingClientRect(); + + return { + top: box.top + (window.pageYOffset || docElem.scrollTop) - (docElem.clientTop || 0), + left: box.left + (window.pageXOffset || docElem.scrollLeft) - (docElem.clientLeft || 0) + }; + } + offsetLeft = elem.offsetLeft; + offsetTop = elem.offsetTop; + lastElem = elem; + + /* jshint ignore:start */ + while (elem = elem.offsetParent) { + // from my observation, document.body always has scrollLeft/scrollTop == 0 + if (elem === document.body) { + break; + } + offsetLeft += elem.offsetLeft; + offsetTop += elem.offsetTop; + lastElem = elem; + } + /* jshint ignore:end */ + + //slow - http://jsperf.com/offset-vs-getboundingclientrect/6 + if (lastElem && lastElem.style.position === 'fixed') { + //if(lastElem !== document.body) { //faster but does gives false positive in Firefox + offsetLeft += window.pageXOffset || docElem.scrollLeft; + offsetTop += window.pageYOffset || docElem.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.getScrollableElement = function (element) { + var el = element.parentNode, + props = ['auto', 'scroll'], + overflow, overflowX, overflowY; + + while (el && el.style) { + overflow = el.style.overflow; + overflowX = el.style.overflowX; + overflowY = el.style.overflowY; + + if (overflow == 'scroll' || overflowX == 'scroll' || overflowY == 'scroll') { + return el; + } + if (el.clientHeight < el.scrollHeight && (props.indexOf(overflowY) !== -1 || props.indexOf(overflow) !== -1)) { + return el; + } + if (el.clientWidth < el.scrollWidth && (props.indexOf(overflowX) !== -1 || props.indexOf(overflow) !== -1)) { + return el; + } + el = el.parentNode; + } + + return window; +}; + +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 || elem.innerHeight; +}; + +Handsontable.Dom.innerWidth = function (elem) { + return elem.clientWidth || elem.innerWidth; +}; + +Handsontable.Dom.addEvent = function(element, event, callback) { + if (window.addEventListener) { + element.addEventListener(event, callback, false); + } else { + element.attachEvent('on' + event, callback); + } +}; + +Handsontable.Dom.removeEvent = function(element, event, callback) { + if (window.removeEventListener) { + element.removeEventListener(event, callback, false); + } else { + element.detachEvent('on' + event, callback); + } +}; + + +(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)) { + this.columnHeaderHeight = Handsontable.Dom.outerHeight(this.instance.wtTable.THEAD); + } + return this.columnHeaderHeight; +}; + +WalkontableViewport.prototype.getViewportHeight = function () { + + var containerHeight = this.getWorkspaceHeight(); + + 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 () { + var containerWidth = this.getWorkspaceWidth(), + rowHeaderWidth; + + if (containerWidth === Infinity) { + return containerWidth; + } + rowHeaderWidth = this.getRowHeaderWidth(); + + if (rowHeaderWidth > 0) { + return containerWidth - rowHeaderWidth; + } + + return containerWidth; +}; + +/** + * Creates: + * - rowsRenderCalculator (before draw, to qualify rows for rendering) + * - rowsVisibleCalculator (after draw, to measure which rows are actually visible) + * @returns {WalkontableViewportRowsCalculator} + */ +WalkontableViewport.prototype.createRowsCalculator = function (visible) { + this.rowHeaderWidth = NaN; + + var height; + if (this.instance.wtSettings.settings.renderAllRows) { + height = Infinity; + } + else { + height = this.getViewportHeight(); + } + + var pos = this.instance.wtScrollbars.vertical.getScrollPosition() - this.instance.wtScrollbars.vertical.getTableParentOffset(); + if (pos < 0) { + pos = 0; + } + + var fixedRowsTop = this.instance.getSetting('fixedRowsTop'); + if(fixedRowsTop) { + var fixedRowsHeight = this.instance.wtScrollbars.vertical.sumCellSizes(0, fixedRowsTop); + pos += fixedRowsHeight; + height -= fixedRowsHeight; + } + + var that = this; + return new WalkontableViewportRowsCalculator( + height, + pos, + this.instance.getSetting('totalRows'), + function(sourceRow) { + return that.instance.wtTable.getRowHeight(sourceRow); + }, + visible ? null : this.instance.wtSettings.settings.viewportRowCalculatorOverride, + visible ? true : false + ); +}; + +/** + * Creates: + * - columnsRenderCalculator (before draw, to qualify columns for rendering) + * - columnsVisibleCalculator (after draw, to measure which columns are actually visible) + * @returns {WalkontableViewportRowsCalculator} + */ +WalkontableViewport.prototype.createColumnsCalculator = function (visible) { + this.columnHeaderHeight = NaN; + + var width = this.getViewportWidth(); + + var pos = this.instance.wtScrollbars.horizontal.getScrollPosition() - this.instance.wtScrollbars.vertical.getTableParentOffset(); + if (pos < 0) { + pos = 0; + } + + var fixedColumnsLeft = this.instance.getSetting('fixedColumnsLeft'); + if(fixedColumnsLeft) { + var fixedColumnsWidth = this.instance.wtScrollbars.horizontal.sumCellSizes(0, fixedColumnsLeft); + pos += fixedColumnsWidth; + width -= fixedColumnsWidth; + } + + var that = this; + return new WalkontableViewportColumnsCalculator( + width, + pos, + this.instance.getSetting('totalColumns'), + function (sourceCol) { + return that.instance.wtTable.getColumnWidth(sourceCol); + }, + visible ? null : this.instance.wtSettings.settings.viewportColumnCalculatorOverride, + visible ? true : false, + this.instance.getSetting('stretchH') + ); +}; + + +/** + * Creates rowsRenderCalculator and columnsRenderCalculator (before draw, to determine what rows and cols should be rendered) + * @param fastDraw {Boolean} If TRUE, will try to avoid full redraw and only update the border positions. If FALSE or UNDEFINED, will perform a full redraw + */ +WalkontableViewport.prototype.createRenderCalculators = function (fastDraw) { + if (fastDraw) { + var proposedRowsVisibleCalculator = this.createRowsCalculator(true); + var proposedColumnsVisibleCalculator = this.createColumnsCalculator(true); + if (!(this.areAllProposedVisibleRowsAlreadyRendered(proposedRowsVisibleCalculator) && this.areAllProposedVisibleColumnsAlreadyRendered(proposedColumnsVisibleCalculator) ) ) { + fastDraw = false; + } + } + + if(!fastDraw) { + this.rowsRenderCalculator = this.createRowsCalculator(); + this.columnsRenderCalculator = this.createColumnsCalculator(); + } + + this.rowsVisibleCalculator = null; //delete temporarily to make sure that renderers always use rowsRenderCalculator, not rowsVisibleCalculator + this.columnsVisibleCalculator = null; + + return fastDraw; +}; + +/** + * Creates rowsVisibleCalculator and columnsVisibleCalculator (after draw, to determine what are the actually visible rows and columns) + */ +WalkontableViewport.prototype.createVisibleCalculators = function () { + this.rowsVisibleCalculator = this.createRowsCalculator(true); + this.columnsVisibleCalculator = this.createColumnsCalculator(true); +}; + +/** + * Returns information whether proposedRowsVisibleCalculator viewport + * is contained inside rows rendered in previous draw (cached in rowsRenderCalculator) + * + * Returns TRUE if all proposed visible rows are already rendered (meaning: redraw is not needed) + * Returns FALSE if at least one proposed visible row is not already rendered (meaning: redraw is needed) + * + * @returns {boolean} + */ +WalkontableViewport.prototype.areAllProposedVisibleRowsAlreadyRendered = function (proposedRowsVisibleCalculator) { + if (this.rowsVisibleCalculator) { + if (proposedRowsVisibleCalculator.startRow < this.rowsRenderCalculator.startRow || + (proposedRowsVisibleCalculator.startRow === this.rowsRenderCalculator.startRow && + proposedRowsVisibleCalculator.startRow > 0)) { + return false; + } + else if (proposedRowsVisibleCalculator.endRow > this.rowsRenderCalculator.endRow || + (proposedRowsVisibleCalculator.endRow === this.rowsRenderCalculator.endRow && + proposedRowsVisibleCalculator.endRow < this.instance.getSetting('totalRows') - 1)) { + return false; + } + else { + return true; + } + } + return false; +}; + +/** + * Returns information whether proposedColumnsVisibleCalculator viewport + * is contained inside column rendered in previous draw (cached in columnsRenderCalculator) + * + * Returns TRUE if all proposed visible columns are already rendered (meaning: redraw is not needed) + * Returns FALSE if at least one proposed visible column is not already rendered (meaning: redraw is needed) + * + * @returns {boolean} + */ +WalkontableViewport.prototype.areAllProposedVisibleColumnsAlreadyRendered = function (proposedColumnsVisibleCalculator) { + if (this.columnsVisibleCalculator) { + if (proposedColumnsVisibleCalculator.startColumn < this.columnsRenderCalculator.startColumn || + (proposedColumnsVisibleCalculator.startColumn === this.columnsRenderCalculator.startColumn && + proposedColumnsVisibleCalculator.startColumn > 0)) { + return false; + } + else if (proposedColumnsVisibleCalculator.endColumn > this.columnsRenderCalculator.endColumn || + (proposedColumnsVisibleCalculator.endColumn === this.columnsRenderCalculator.endColumn && + proposedColumnsVisibleCalculator.endColumn < this.instance.getSetting('totalColumns') - 1)) { + return false; + } + else { + return true; + } + } + return false; +}; + +function WalkontableViewportColumnsCalculator (width, scrollOffset, totalColumns, columnWidthFn, overrideFn, onlyFullyVisible, stretchH) { + var + _this = this, + ratio = 1, + sum = 0, + needReverse = true, + defaultColumnWidth = 50, + startPositions = [], + getColumnWidth, + columnWidth, i; + + this.scrollOffset = scrollOffset; + this.startColumn = null; + this.endColumn = null; + this.startPosition = null; + this.count = 0; + this.stretchAllRatio = 0; + this.stretchLastWidth = 0; + this.stretch = stretchH; + this.totalTargetWidth = 0; + this.needVerifyLastColumnWidth = true; + this.stretchAllColumnsWidth = []; + + + function getStretchedAllColumnWidth(column, baseWidth) { + var sumRatioWidth = 0; + + if (!_this.stretchAllColumnsWidth[column]) { + _this.stretchAllColumnsWidth[column] = Math.round(baseWidth * _this.stretchAllRatio); + } + if (_this.stretchAllColumnsWidth.length === totalColumns && _this.needVerifyLastColumnWidth) { + _this.needVerifyLastColumnWidth = false; + + for (var i = 0; i < _this.stretchAllColumnsWidth.length; i++) { + sumRatioWidth += _this.stretchAllColumnsWidth[i]; + } + if (sumRatioWidth != _this.totalTargetWidth) { + _this.stretchAllColumnsWidth[_this.stretchAllColumnsWidth.length - 1] += _this.totalTargetWidth - sumRatioWidth; + } + } + + return _this.stretchAllColumnsWidth[column]; + } + + function getStretchedLastColumnWidth(column, baseWidth) { + if (column === totalColumns - 1) { + return _this.stretchLastWidth; + } + + return null; + } + + getColumnWidth = function getColumnWidth(i) { + var width = columnWidthFn(i); + + ratio = ratio || 1; + + if (width === undefined) { + width = defaultColumnWidth; + } + + return width; + }; + + /** + * Recalculate columns stretching. + * + * @param {Number} totalWidth + */ + this.refreshStretching = function (totalWidth) { + var sumAll = 0, + columnWidth, + remainingSize; + + for (var i = 0; i < totalColumns; i++) { + columnWidth = getColumnWidth(i); + sumAll += columnWidth; + } + this.totalTargetWidth = totalWidth; + remainingSize = sumAll - totalWidth; + + if (this.stretch === 'all' && remainingSize < 0) { + this.stretchAllRatio = totalWidth / sumAll; + this.stretchAllColumnsWidth = []; + this.needVerifyLastColumnWidth = true; + } + else if (this.stretch === 'last' && totalWidth !== Infinity) { + this.stretchLastWidth = -remainingSize + getColumnWidth(totalColumns - 1); + } + }; + + /** + * Get stretched column width based on stretchH (all or last) setting passed in handsontable instance. + * + * @param {Number} column + * @param {Number} baseWidth + * @returns {Number|null} + */ + this.getStretchedColumnWidth = function(column, baseWidth) { + var result = null; + + if (this.stretch === 'all' && this.stretchAllRatio !== 0) { + result = getStretchedAllColumnWidth(column, baseWidth); + } + else if (this.stretch === 'last' && this.stretchLastWidth !== 0) { + result = getStretchedLastColumnWidth(column, baseWidth); + } + + return result; + }; + + + for (i = 0; i < totalColumns; i++) { + columnWidth = getColumnWidth(i); + + if (sum <= scrollOffset && !onlyFullyVisible) { + this.startColumn = i; + } + + if (sum >= scrollOffset && sum + columnWidth <= scrollOffset + width) { + if (this.startColumn == null) { + this.startColumn = i; + } + this.endColumn = i; + } + startPositions.push(sum); + sum += columnWidth; + + if (!onlyFullyVisible) { + this.endColumn = i; + } + if (sum >= scrollOffset + width) { + needReverse = false; + break; + } + } + + if (this.endColumn == totalColumns - 1 && needReverse) { + this.startColumn = this.endColumn; + + while (this.startColumn > 0) { + var viewportSum = startPositions[this.endColumn] + columnWidth - startPositions[this.startColumn - 1]; + + if (viewportSum <= width || !onlyFullyVisible) { + this.startColumn--; + } + if (viewportSum > width) { + break; + } + } + } + + if (this.startColumn !== null && overrideFn) { + overrideFn(this); + } + this.startPosition = startPositions[this.startColumn]; + + if (this.startPosition == void 0) { + this.startPosition = null; + } + if (this.startColumn != null) { + this.count = this.endColumn - this.startColumn + 1; + } +} + + +/** + * Viewport calculator constructor. Calculates indexes of rows to render OR rows that are visible. + * To redo the calculation, you need to create a new calculator. + * + * Object properties: + * this.scrollOffset - position of vertical scroll (in px) + * this.startRow - index of the first rendered/visible row (can be overwritten using overrideFn) + * this.startPosition - position of the first rendered/visible row (in px) + * this.endRow - index of the last rendered/visible row (can be overwritten using overrideFn) + * this.count - number of rendered/visible rows + * + * @param height - height of the viewport + * @param scrollOffset - current vertical scroll position of the viewport + * @param totalRows - total number of rows + * @param rowHeightFn - function that returns the height of the row at a given index (in px) + * @param overrideFn - function that changes calculated this.startRow, this.endRow (used by mergeCells.js plugin) + * @param onlyFullyVisible {bool} - if TRUE, only startRow and endRow will be indexes of rows that are FULLY in viewport + * @constructor + */ +function WalkontableViewportRowsCalculator(height, scrollOffset, totalRows, rowHeightFn, overrideFn, onlyFullyVisible) { + this.scrollOffset = scrollOffset; + this.startRow = null; + this.startPosition = null; + this.endRow = null; + this.count = 0; + var sum = 0; + var rowHeight; + var needReverse = true; + var defaultRowHeight = 23; + var startPositions = []; + for (var i = 0; i < totalRows; i++) { + rowHeight = rowHeightFn(i); + if (rowHeight === undefined) { + rowHeight = defaultRowHeight; + } + if (sum <= scrollOffset && !onlyFullyVisible) { + this.startRow = i; + } + if (sum >= scrollOffset && sum + rowHeight <= scrollOffset + height) { + if (this.startRow == null) { + this.startRow = i; + } + this.endRow = i; + } + startPositions.push(sum); + sum += rowHeight; + if(!onlyFullyVisible) { + this.endRow = i; + } + if (sum >= scrollOffset + height) { + needReverse = false; + break; + } + } + + //If the rendering has reached the last row and there is still some space available in the viewport, we need to render in reverse in order to fill the whole viewport with rows + if (this.endRow == totalRows - 1 && needReverse) { + this.startRow = this.endRow; + while(this.startRow > 0) { + var viewportSum = startPositions[this.endRow] + rowHeight - startPositions[this.startRow - 1]; //rowHeight is the height of the last row + if (viewportSum <= height || !onlyFullyVisible) + { + this.startRow--; + } + if (viewportSum >= height) + { + break; + } + } + } + + if (this.startRow !== null && overrideFn) { + overrideFn(this); + } + + this.startPosition = startPositions[this.startRow]; + if (this.startPosition == void 0) { + this.startPosition = null; + } + + if (this.startRow != null) { + this.count = this.endRow - this.startRow + 1; + } +} + +if (window.jQuery) { + (function (window, $, Handsontable) { + $.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[0], 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); + + if (action === 'destroy'){ + $this.removeData(); + } + } + else { + throw new Error('Handsontable do not provide action: ' + action); + } + } + + return output; + } + }; + })(window, jQuery, Handsontable); +} + + + +})(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; + } + + key = key.toLowerCase(); + + 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'; + }; + + numeral.validate = function(val, culture) { + + var _decimalSep, + _thousandSep, + _currSymbol, + _valArray, + _abbrObj, + _thousandRegEx, + languageData, + temp; + + //coerce val to string + if (typeof val !== 'string') { + val += ''; + if (console.warn) { + console.warn('Numeral.js: Value is not string. It has been co-erced to: ', val); + } + } + + //trim whitespaces from either sides + val = val.trim(); + + + //if val is empty return false + if (val === '') { + return false; + } + + //replace the initial '+' or '-' sign if present + val = val.replace(/^[+-]?/, ''); + + + //get the decimal and thousands separator from numeral.languageData + try { + //check if the culture is understood by numeral. if not, default it to current language + languageData = numeral.languageData(culture); + } catch (e) { + languageData = numeral.languageData(numeral.language()); + } + + //setup the delimiters and currency symbol based on culture/language + _currSymbol = languageData.currency.symbol; + _abbrObj = languageData.abbreviations; + _decimalSep = languageData.delimiters.decimal; + if (languageData.delimiters.thousands === '.') { + _thousandSep = '\\.'; + } else { + _thousandSep = languageData.delimiters.thousands; + } + + //validating currency symbol + temp = val.match(/^[^\d]+/); + if (temp !== null) { + //chuck the currency symbol away + val = val.substr(1); + if (temp[0] !== _currSymbol) { + return false; + } + } + + //validating abbreviation symbol + temp = val.match(/[^\d]+$/); + if (temp !== null) { + val = val.slice(0, - 1); + if (temp[0] !== _abbrObj.thousand && temp[0] !== _abbrObj.million && temp[0] !== _abbrObj.billion && temp[0] !== _abbrObj.trillion) { + return false; + } + } + + //if val is just digits the return true + if ( !! val.match(/^\d+$/)) { + return true; + } + _thousandRegEx = new RegExp(_thousandSep + '{2}'); + + if (!val.match(/[^\d.,]/g)) { + _valArray = val.split(_decimalSep); + if (_valArray.length > 2) { + return false; + } else { + if (_valArray.length < 2) { + return ( !! _valArray[0].match(/^\d+.*\d$/) && !_valArray[0].match(_thousandRegEx)); + } else { + if (_valArray[0].length === 1) { + return ( !! _valArray[0].match(/^\d+$/) && !_valArray[0].match(_thousandRegEx) && !! _valArray[1].match(/^\d+$/)); + } else { + return ( !! _valArray[0].match(/^\d+.*\d$/) && !_valArray[0].match(_thousandRegEx) && !! _valArray[1].match(/^\d+$/)); + } + } + } + } + + return false; + }; + + /************************************ + 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/package.js b/package.js new file mode 100644 index 0000000..2863dce --- /dev/null +++ b/package.js @@ -0,0 +1,25 @@ +Package.describe({ + name: 'awsp:handsontable', + version: '0.3.1', + summary: 'Quote from origin: Handsontable is a minimalist Excel-like data grid editor for HTML & JavaScript', + git: '', + documentation: 'README.md' +}); + +Package.onUse(function(api) { + api.versionsFrom('1.0'); + api.addFiles('awsp:handsontable.js'); + api.use('jquery', 'client'); + api.addFiles([ + 'lib/handsontable.meteor.js', + 'bower_components/handsontable/dist/handsontable.full.css', + ], 'client'); + + api.export('Handsontable', 'client'); +}); + +Package.onTest(function(api) { + api.use('tinytest'); + api.use('awsp:handsontable'); + api.addFiles('awsp:handsontable-tests.js'); +});