From 39e7af623839b13837c56d11a16582104d100813 Mon Sep 17 00:00:00 2001 From: baldo Date: Mon, 16 May 2016 13:33:49 +0200 Subject: [PATCH] Lots of updates --- Gruntfile.js | 25 +- .../angular-bootstrap/.bower.json | 25 +- .../angular-bootstrap/.gitignore | 1 + .../angular-bootstrap/.npmignore | 1 + .../angular-bootstrap/README.md | 120 + .../angular-bootstrap/bower.json | 14 +- .../angular-bootstrap/index.js | 2 + .../angular-bootstrap/package.json | 23 + .../angular-bootstrap/ui-bootstrap-csp.css | 115 + .../angular-bootstrap/ui-bootstrap-tpls.js | 8407 ++-- .../ui-bootstrap-tpls.min.js | 10 +- .../angular-bootstrap/ui-bootstrap.js | 7903 ++-- .../angular-bootstrap/ui-bootstrap.min.js | 9 +- .../angular-leaflet-directive/.bower.json | 59 +- .../angular-leaflet-directive/LICENSE | 2 +- .../angular-leaflet-directive/bower.json | 64 + .../angular-leaflet-directive/coffeelint.json | 114 + .../dist/angular-leaflet-directive.js | 8286 ++-- .../dist/angular-leaflet-directive.min.js | 14 +- .../angular-leaflet-directive.no-header.js | 5705 +++ .../generate-examples.js | 205 + .../grunt/aliases.yaml | 54 + .../grunt/clean.json | 4 + .../grunt/concat.json | 36 + .../grunt/connect.json | 18 + .../angular-leaflet-directive/grunt/jscs.json | 7 + .../grunt/jshint.json | 14 + .../grunt/karma.json | 7 + .../grunt/ngAnnotate.json | 8 + .../angular-leaflet-directive/grunt/open.json | 8 + .../grunt/protractor.json | 20 + .../grunt/shell.json | 21 + .../grunt/uglify.json | 11 + .../grunt/watch.json | 9 + .../angular-mocks/.bower.json | 16 +- app/bower_components/angular-mocks/README.md | 31 +- .../angular-mocks/angular-mocks.js | 1641 +- app/bower_components/angular-mocks/bower.json | 6 +- .../angular-mocks/ngAnimateMock.js | 2 + app/bower_components/angular-mocks/ngMock.js | 2 + .../angular-mocks/ngMockE2E.js | 2 + .../angular-mocks/package.json | 27 + .../angular-route/.bower.json | 16 +- app/bower_components/angular-route/README.md | 24 +- .../angular-route/angular-route.js | 286 +- .../angular-route/angular-route.min.js | 21 +- .../angular-route/angular-route.min.js.map | 6 +- app/bower_components/angular-route/bower.json | 6 +- app/bower_components/angular-route/index.js | 2 + .../angular-route/package.json | 26 + .../angular-sanitize/.bower.json | 16 +- .../angular-sanitize/README.md | 24 +- .../angular-sanitize/angular-sanitize.js | 621 +- .../angular-sanitize/angular-sanitize.min.js | 22 +- .../angular-sanitize.min.js.map | 4 +- .../angular-sanitize/bower.json | 6 +- .../angular-sanitize/index.js | 2 + .../angular-sanitize/package.json | 26 + .../angular-scenario/.bower.json | 16 +- .../angular-scenario/README.md | 27 +- .../angular-scenario/angular-scenario.js | 35396 ++++++++++------ .../angular-scenario/bower.json | 6 +- .../angular-scenario/package.json | 26 + app/bower_components/angular/.bower.json | 14 +- app/bower_components/angular/README.md | 26 +- app/bower_components/angular/angular-csp.css | 13 +- app/bower_components/angular/angular.js | 22586 +++++++--- app/bower_components/angular/angular.min.js | 524 +- .../angular/angular.min.js.gzip | Bin 39367 -> 56091 bytes .../angular/angular.min.js.map | 6 +- app/bower_components/angular/bower.json | 4 +- app/bower_components/angular/index.js | 2 + app/bower_components/angular/package.json | 25 + .../bootstrap-sass-official/.bower.json | 34 +- .../bootstrap-sass-official/CHANGELOG.md | 68 + .../bootstrap-sass-official/CONTRIBUTING.md | 11 +- .../bootstrap-sass-official/LICENSE | 2 +- .../bootstrap-sass-official/README.md | 172 +- .../glyphicons-halflings-regular.eot | Bin 20335 -> 20127 bytes .../glyphicons-halflings-regular.svg | 485 +- .../glyphicons-halflings-regular.ttf | Bin 41280 -> 45404 bytes .../glyphicons-halflings-regular.woff | Bin 23320 -> 23424 bytes .../glyphicons-halflings-regular.woff2 | Bin 0 -> 18028 bytes .../assets/javascripts/bootstrap-sprockets.js | 4 +- .../assets/javascripts/bootstrap.js | 2728 +- .../assets/javascripts/bootstrap.min.js | 7 + .../assets/javascripts/bootstrap/affix.js | 72 +- .../assets/javascripts/bootstrap/alert.js | 12 +- .../assets/javascripts/bootstrap/button.js | 44 +- .../assets/javascripts/bootstrap/carousel.js | 56 +- .../assets/javascripts/bootstrap/collapse.js | 95 +- .../assets/javascripts/bootstrap/dropdown.js | 96 +- .../assets/javascripts/bootstrap/modal.js | 119 +- .../assets/javascripts/bootstrap/popover.js | 15 +- .../assets/javascripts/bootstrap/scrollspy.js | 54 +- .../assets/javascripts/bootstrap/tab.js | 63 +- .../assets/javascripts/bootstrap/tooltip.js | 165 +- .../javascripts/bootstrap/transition.js | 4 +- .../stylesheets/_bootstrap-compass.scss | 2 + .../assets/stylesheets/_bootstrap-mincer.scss | 6 +- .../stylesheets/_bootstrap-sprockets.scss | 2 + .../{bootstrap.scss => _bootstrap.scss} | 6 + .../assets/stylesheets/bootstrap/_alerts.scss | 5 + .../assets/stylesheets/bootstrap/_badges.scss | 17 +- .../stylesheets/bootstrap/_breadcrumbs.scss | 4 +- .../stylesheets/bootstrap/_button-groups.scss | 42 +- .../stylesheets/bootstrap/_buttons.scss | 33 +- .../stylesheets/bootstrap/_carousel.scss | 43 +- .../assets/stylesheets/bootstrap/_close.scss | 1 + .../assets/stylesheets/bootstrap/_code.scss | 1 + .../bootstrap/_component-animations.scss | 4 +- .../stylesheets/bootstrap/_dropdowns.scss | 19 +- .../assets/stylesheets/bootstrap/_forms.scss | 187 +- .../stylesheets/bootstrap/_glyphicons.scss | 100 +- .../assets/stylesheets/bootstrap/_grid.scss | 8 +- .../stylesheets/bootstrap/_input-groups.scss | 11 +- .../stylesheets/bootstrap/_jumbotron.scss | 14 +- .../stylesheets/bootstrap/_list-group.scss | 24 +- .../assets/stylesheets/bootstrap/_media.scss | 76 +- .../assets/stylesheets/bootstrap/_mixins.scss | 1 + .../assets/stylesheets/bootstrap/_modals.scss | 8 +- .../assets/stylesheets/bootstrap/_navbar.scss | 69 +- .../assets/stylesheets/bootstrap/_navs.scss | 6 +- .../stylesheets/bootstrap/_normalize.scss | 25 +- .../assets/stylesheets/bootstrap/_pager.scss | 5 +- .../stylesheets/bootstrap/_pagination.scss | 9 +- .../assets/stylesheets/bootstrap/_panels.scss | 38 +- .../stylesheets/bootstrap/_popovers.scss | 12 +- .../assets/stylesheets/bootstrap/_print.scss | 188 +- .../stylesheets/bootstrap/_progress-bars.scss | 22 +- .../bootstrap/_responsive-embed.scss | 23 +- .../bootstrap/_responsive-utilities.scss | 11 +- .../stylesheets/bootstrap/_scaffolding.scss | 17 +- .../assets/stylesheets/bootstrap/_tables.scss | 27 +- .../assets/stylesheets/bootstrap/_theme.scss | 69 +- .../stylesheets/bootstrap/_thumbnails.scss | 4 +- .../stylesheets/bootstrap/_tooltip.scss | 20 +- .../assets/stylesheets/bootstrap/_type.scss | 30 +- .../stylesheets/bootstrap/_utilities.scss | 8 +- .../stylesheets/bootstrap/_variables.scss | 72 +- .../stylesheets/bootstrap/bootstrap.scss | 50 - .../bootstrap/mixins/_background-variant.scss | 3 +- .../bootstrap/mixins/_buttons.scss | 23 +- .../stylesheets/bootstrap/mixins/_forms.scss | 6 +- .../bootstrap/mixins/_grid-framework.scss | 4 +- .../stylesheets/bootstrap/mixins/_grid.scss | 12 +- .../bootstrap/mixins/_hide-text.scss | 8 +- .../stylesheets/bootstrap/mixins/_image.scss | 1 - .../stylesheets/bootstrap/mixins/_labels.scss | 2 +- .../bootstrap/mixins/_list-group.scss | 5 +- .../bootstrap/mixins/_opacity.scss | 2 +- .../bootstrap/mixins/_pagination.scss | 3 +- .../bootstrap/mixins/_progress-bar.scss | 2 +- .../bootstrap/mixins/_reset-text.scss | 18 + .../mixins/_responsive-visibility.scss | 2 +- .../bootstrap/mixins/_text-emphasis.scss | 3 +- .../bootstrap/mixins/_vendor-prefixes.scss | 13 +- .../bootstrap-sass-official/bower.json | 26 +- .../bootstrap-sass-official/composer.json | 4 +- .../bootstrap-sass-official/package.json | 11 +- .../bootstrap-sass-official/sache.json | 2 +- app/bower_components/es5-shim/.bower.json | 15 +- app/bower_components/es5-shim/CHANGES | 244 + app/bower_components/es5-shim/LICENSE | 2 +- app/bower_components/es5-shim/Makefile | 63 + app/bower_components/es5-shim/README.md | 57 +- app/bower_components/es5-shim/bower.json | 4 +- app/bower_components/es5-shim/component.json | 5 +- app/bower_components/es5-shim/es5-sham.js | 239 +- app/bower_components/es5-shim/es5-sham.map | 2 +- app/bower_components/es5-shim/es5-sham.min.js | 8 +- app/bower_components/es5-shim/es5-shim.js | 1790 +- app/bower_components/es5-shim/es5-shim.map | 2 +- app/bower_components/es5-shim/es5-shim.min.js | 8 +- app/bower_components/es5-shim/package.json | 44 +- app/bower_components/font-awesome/.bower.json | 16 +- app/bower_components/font-awesome/.gitignore | 1 + .../font-awesome/HELP-US-OUT.txt | 7 + app/bower_components/font-awesome/bower.json | 22 + .../font-awesome/css/font-awesome.css | 727 +- .../font-awesome/css/font-awesome.css.map | 7 + .../font-awesome/css/font-awesome.min.css | 4 +- .../font-awesome/fonts/FontAwesome.otf | Bin 75188 -> 124988 bytes .../fonts/fontawesome-webfont.eot | Bin 72449 -> 76518 bytes .../fonts/fontawesome-webfont.svg | 1107 +- .../fonts/fontawesome-webfont.ttf | Bin 141564 -> 152796 bytes .../fonts/fontawesome-webfont.woff | Bin 83760 -> 90412 bytes .../fonts/fontawesome-webfont.woff2 | Bin 0 -> 71896 bytes .../font-awesome/less/animated.less | 34 + .../font-awesome/less/bordered-pulled.less | 9 + .../font-awesome/less/core.less | 8 +- .../font-awesome/less/extras.less | 2 - .../font-awesome/less/font-awesome.less | 5 +- .../font-awesome/less/icons.less | 235 +- .../font-awesome/less/list.less | 2 +- .../font-awesome/less/mixins.less | 52 +- .../font-awesome/less/path.less | 13 +- .../font-awesome/less/rotated-flipped.less | 11 + .../font-awesome/less/screen-reader.less | 5 + .../font-awesome/less/spinning.less | 32 - .../font-awesome/less/variables.less | 237 +- .../font-awesome/scss/_animated.scss | 34 + .../font-awesome/scss/_bordered-pulled.scss | 9 + .../font-awesome/scss/_core.scss | 8 +- .../font-awesome/scss/_extras.scss | 44 - .../font-awesome/scss/_icons.scss | 235 +- .../font-awesome/scss/_mixins.scss | 52 +- .../font-awesome/scss/_path.scss | 3 +- .../font-awesome/scss/_rotated-flipped.scss | 11 + .../font-awesome/scss/_screen-reader.scss | 5 + .../font-awesome/scss/_spinning.scss | 32 - .../font-awesome/scss/_variables.scss | 237 +- .../font-awesome/scss/font-awesome.scss | 5 +- app/bower_components/geolib/.bower.json | 12 +- app/bower_components/geolib/Gruntfile.js | 40 +- app/bower_components/geolib/README.md | 189 +- app/bower_components/geolib/bower.json | 2 +- app/bower_components/geolib/component.json | 11 +- .../geolib/dist/geolib.elevation.js | 4 +- .../geolib/dist/geolib.elevation.min.js | 6 +- .../geolib/dist/geolib.isPointInsideRobust.js | 744 + .../dist/geolib.isPointInsideRobust.min.js | 15 + app/bower_components/geolib/dist/geolib.js | 2332 +- .../geolib/dist/geolib.min.js | 6 +- app/bower_components/geolib/index.html | 1 + app/bower_components/geolib/meteor/README.md | 25 + app/bower_components/geolib/meteor/export.js | 3 + app/bower_components/geolib/meteor/package.js | 30 + app/bower_components/geolib/meteor/test.js | 5 + app/bower_components/geolib/package.json | 11 +- .../geolib/src/geolib.isPointInsideRobust.js | 744 + app/bower_components/geolib/src/geolib.js | 2328 +- app/bower_components/jquery/.bower.json | 17 +- app/bower_components/jquery/bower.json | 5 +- app/bower_components/jquery/dist/jquery.js | 544 +- .../jquery/dist/jquery.min.js | 8 +- .../jquery/dist/jquery.min.map | 2 +- app/bower_components/jquery/src/ajax.js | 40 +- app/bower_components/jquery/src/ajax/xhr.js | 5 +- .../jquery/src/attributes/attr.js | 2 - .../jquery/src/attributes/classes.js | 8 +- .../jquery/src/attributes/prop.js | 4 +- .../jquery/src/attributes/support.js | 16 +- .../jquery/src/attributes/val.js | 8 +- app/bower_components/jquery/src/core.js | 20 +- app/bower_components/jquery/src/core/init.js | 6 +- app/bower_components/jquery/src/core/ready.js | 2 +- app/bower_components/jquery/src/css.js | 57 +- .../jquery/src/css/addGetHookIf.js | 6 +- app/bower_components/jquery/src/css/curCSS.js | 2 +- .../jquery/src/css/defaultDisplay.js | 2 +- .../jquery/src/css/support.js | 5 + .../jquery/src/css/var/getStyles.js | 9 +- app/bower_components/jquery/src/data.js | 19 +- app/bower_components/jquery/src/data/Data.js | 6 +- app/bower_components/jquery/src/deferred.js | 4 +- app/bower_components/jquery/src/dimensions.js | 2 +- app/bower_components/jquery/src/effects.js | 59 +- .../jquery/src/effects/Tween.js | 14 +- app/bower_components/jquery/src/event.js | 10 +- app/bower_components/jquery/src/event/ajax.js | 13 + .../jquery/src/exports/global.js | 4 +- app/bower_components/jquery/src/intro.js | 16 +- app/bower_components/jquery/src/jquery.js | 1 + .../jquery/src/manipulation.js | 20 +- .../jquery/src/manipulation/support.js | 11 +- app/bower_components/jquery/src/offset.js | 15 +- app/bower_components/jquery/src/queue.js | 6 +- .../jquery/src/sizzle/dist/sizzle.js | 143 +- .../jquery/src/sizzle/dist/sizzle.min.js | 6 +- .../jquery/src/sizzle/dist/sizzle.min.map | 2 +- app/bower_components/jquery/src/traversing.js | 7 +- app/bower_components/jquery/src/wrap.js | 1 + app/bower_components/json3/.bower.json | 2 +- app/bower_components/leaflet-dist/.bower.json | 2 +- app/bower_components/leaflet/.bower.json | 33 + app/bower_components/leaflet/CHANGELOG.md | 957 + app/bower_components/leaflet/CONTRIBUTING.md | 155 + app/bower_components/leaflet/Jakefile.js | 48 + app/bower_components/leaflet/LICENSE | 23 + app/bower_components/leaflet/PLUGIN-GUIDE.md | 127 + app/bower_components/leaflet/README.md | 34 + app/bower_components/leaflet/bower.json | 23 + app/bower_components/leaflet/component.json | 20 + .../leaflet/dist/images/layers-2x.png | Bin 0 -> 2898 bytes .../leaflet/dist/images/layers.png | Bin 0 -> 1502 bytes .../leaflet/dist/images/marker-icon-2x.png | Bin 0 -> 4033 bytes .../leaflet/dist/images/marker-icon.png | Bin 0 -> 1747 bytes .../leaflet/dist/images/marker-shadow.png | Bin 0 -> 797 bytes .../leaflet/dist/leaflet-src.js | 9168 ++++ app/bower_components/leaflet/dist/leaflet.css | 479 + app/bower_components/leaflet/dist/leaflet.js | 9 + app/bower_components/leaflet/package.json | 33 + app/bower_components/lodash/.bower.json | 14 + .../{underscore => lodash}/.editorconfig | 10 +- app/bower_components/lodash/.gitattributes | 1 + .../lodash/.github/CONTRIBUTING.md | 78 + app/bower_components/lodash/.gitignore | 9 + app/bower_components/lodash/.jscsrc | 97 + .../lodash/.markdown-doctest-setup.js | 44 + app/bower_components/lodash/.travis.yml | 86 + app/bower_components/lodash/LICENSE | 47 + app/bower_components/lodash/README.md | 46 + .../lodash/dist/lodash.core.js | 3926 ++ .../lodash/dist/lodash.core.min.js | 28 + app/bower_components/lodash/dist/lodash.fp.js | 860 + .../lodash/dist/lodash.fp.min.js | 16 + app/bower_components/lodash/dist/lodash.js | 16242 +++++++ .../lodash/dist/lodash.min.js | 125 + .../lodash/dist/mapping.fp.js | 352 + app/bower_components/lodash/doc/README.md | 10774 +++++ .../lodash/fp/_baseConvert.js | 466 + .../lodash/fp/_convertBrowser.js | 18 + app/bower_components/lodash/fp/_mapping.js | 290 + app/bower_components/lodash/fp/placeholder.js | 6 + .../lodash/lib/common/file.js | 71 + .../lodash/lib/common/mapping.js | 9 + .../lodash/lib/common/minify.js | 39 + .../lodash/lib/common/uglify.options.js | 23 + .../lodash/lib/common/util.js | 27 + .../lodash/lib/fp/build-dist.js | 55 + .../lodash/lib/fp/build-doc.js | 65 + .../lodash/lib/fp/build-modules.js | 120 + .../lodash/lib/fp/template/doc/wiki.jst | 227 + .../lib/fp/template/modules/_falseOptions.jst | 7 + .../lodash/lib/fp/template/modules/_util.jst | 14 + .../lodash/lib/fp/template/modules/alias.jst | 1 + .../lib/fp/template/modules/category.jst | 2 + .../lib/fp/template/modules/convert.jst | 18 + .../lodash/lib/fp/template/modules/fp.jst | 2 + .../lodash/lib/fp/template/modules/module.jst | 5 + .../lodash/lib/fp/template/modules/thru.jst | 5 + .../lodash/lib/main/build-dist.js | 30 + .../lodash/lib/main/build-doc.js | 60 + .../lodash/lib/main/build-modules.js | 34 + app/bower_components/lodash/lodash.js | 16242 +++++++ app/bower_components/lodash/package.json | 54 + .../lodash/perf/asset/perf-ui.js | 131 + app/bower_components/lodash/perf/index.html | 69 + app/bower_components/lodash/perf/perf.js | 1977 + .../lodash/test/asset/test-ui.js | 170 + .../lodash/test/asset/worker.js | 15 + .../lodash/test/backbone.html | 170 + app/bower_components/lodash/test/fp.html | 41 + app/bower_components/lodash/test/index.html | 351 + app/bower_components/lodash/test/remove.js | 27 + app/bower_components/lodash/test/saucelabs.js | 914 + app/bower_components/lodash/test/test-fp.js | 1799 + app/bower_components/lodash/test/test.js | 26351 ++++++++++++ .../lodash/test/underscore.html | 484 + .../lodash/vendor/backbone/LICENSE | 22 + .../lodash/vendor/backbone/backbone.js | 1920 + .../lodash/vendor/backbone/test/collection.js | 1998 + .../lodash/vendor/backbone/test/events.js | 706 + .../lodash/vendor/backbone/test/model.js | 1418 + .../lodash/vendor/backbone/test/noconflict.js | 13 + .../lodash/vendor/backbone/test/router.js | 1062 + .../vendor/backbone/test/setup/dom-setup.js | 4 + .../vendor/backbone/test/setup/environment.js | 45 + .../lodash/vendor/backbone/test/sync.js | 239 + .../lodash/vendor/backbone/test/view.js | 495 + .../lodash/vendor/firebug-lite/license.txt | 30 + .../vendor/firebug-lite/skin/xp/blank.gif | Bin 0 -> 43 bytes .../vendor/firebug-lite/skin/xp/buttonBg.png | Bin 0 -> 167 bytes .../firebug-lite/skin/xp/buttonBgHover.png | Bin 0 -> 171 bytes .../vendor/firebug-lite/skin/xp/debugger.css | 331 + .../vendor/firebug-lite/skin/xp/detach.png | Bin 0 -> 655 bytes .../firebug-lite/skin/xp/detachHover.png | Bin 0 -> 586 bytes .../vendor/firebug-lite/skin/xp/disable.gif | Bin 0 -> 340 bytes .../vendor/firebug-lite/skin/xp/disable.png | Bin 0 -> 543 bytes .../firebug-lite/skin/xp/disableHover.gif | Bin 0 -> 344 bytes .../firebug-lite/skin/xp/disableHover.png | Bin 0 -> 512 bytes .../vendor/firebug-lite/skin/xp/down.png | Bin 0 -> 637 bytes .../firebug-lite/skin/xp/downActive.png | Bin 0 -> 543 bytes .../vendor/firebug-lite/skin/xp/downHover.png | Bin 0 -> 526 bytes .../firebug-lite/skin/xp/errorIcon-sm.png | Bin 0 -> 447 bytes .../vendor/firebug-lite/skin/xp/errorIcon.gif | Bin 0 -> 365 bytes .../vendor/firebug-lite/skin/xp/errorIcon.png | Bin 0 -> 457 bytes .../firebug-lite/skin/xp/firebug-1.3a2.css | 817 + .../firebug-lite/skin/xp/firebug.IE6.css | 20 + .../vendor/firebug-lite/skin/xp/firebug.css | 3147 ++ .../vendor/firebug-lite/skin/xp/firebug.html | 215 + .../vendor/firebug-lite/skin/xp/firebug.png | Bin 0 -> 1167 bytes .../vendor/firebug-lite/skin/xp/group.gif | Bin 0 -> 158 bytes .../vendor/firebug-lite/skin/xp/html.css | 272 + .../vendor/firebug-lite/skin/xp/infoIcon.gif | Bin 0 -> 359 bytes .../vendor/firebug-lite/skin/xp/infoIcon.png | Bin 0 -> 524 bytes .../firebug-lite/skin/xp/loading_16.gif | Bin 0 -> 1553 bytes .../vendor/firebug-lite/skin/xp/min.png | Bin 0 -> 552 bytes .../vendor/firebug-lite/skin/xp/minHover.png | Bin 0 -> 485 bytes .../vendor/firebug-lite/skin/xp/off.png | Bin 0 -> 742 bytes .../vendor/firebug-lite/skin/xp/offHover.png | Bin 0 -> 680 bytes .../skin/xp/pixel_transparent.gif | Bin 0 -> 43 bytes .../firebug-lite/skin/xp/roundCorner.svg | 6 + .../vendor/firebug-lite/skin/xp/search.gif | Bin 0 -> 550 bytes .../vendor/firebug-lite/skin/xp/search.png | Bin 0 -> 685 bytes .../vendor/firebug-lite/skin/xp/shadow.gif | Bin 0 -> 4364 bytes .../vendor/firebug-lite/skin/xp/shadow2.gif | Bin 0 -> 3093 bytes .../firebug-lite/skin/xp/shadowAlpha.png | Bin 0 -> 3403 bytes .../vendor/firebug-lite/skin/xp/sprite.png | Bin 0 -> 40027 bytes .../firebug-lite/skin/xp/tabHoverLeft.png | Bin 0 -> 438 bytes .../firebug-lite/skin/xp/tabHoverMid.png | Bin 0 -> 261 bytes .../firebug-lite/skin/xp/tabHoverRight.png | Bin 0 -> 436 bytes .../vendor/firebug-lite/skin/xp/tabLeft.png | Bin 0 -> 449 bytes .../firebug-lite/skin/xp/tabMenuCheckbox.png | Bin 0 -> 220 bytes .../firebug-lite/skin/xp/tabMenuPin.png | Bin 0 -> 207 bytes .../firebug-lite/skin/xp/tabMenuRadio.png | Bin 0 -> 192 bytes .../firebug-lite/skin/xp/tabMenuTarget.png | Bin 0 -> 142 bytes .../skin/xp/tabMenuTargetHover.png | Bin 0 -> 148 bytes .../vendor/firebug-lite/skin/xp/tabMid.png | Bin 0 -> 262 bytes .../vendor/firebug-lite/skin/xp/tabRight.png | Bin 0 -> 448 bytes .../skin/xp/textEditorBorders.gif | Bin 0 -> 117 bytes .../skin/xp/textEditorBorders.png | Bin 0 -> 3144 bytes .../skin/xp/textEditorCorners.gif | Bin 0 -> 1821 bytes .../skin/xp/textEditorCorners.png | Bin 0 -> 3960 bytes .../firebug-lite/skin/xp/titlebarMid.png | Bin 0 -> 273 bytes .../firebug-lite/skin/xp/toolbarMid.png | Bin 0 -> 242 bytes .../firebug-lite/skin/xp/tree_close.gif | Bin 0 -> 300 bytes .../vendor/firebug-lite/skin/xp/tree_open.gif | Bin 0 -> 202 bytes .../firebug-lite/skin/xp/twistyClosed.png | Bin 0 -> 334 bytes .../firebug-lite/skin/xp/twistyOpen.png | Bin 0 -> 309 bytes .../lodash/vendor/firebug-lite/skin/xp/up.png | Bin 0 -> 619 bytes .../vendor/firebug-lite/skin/xp/upActive.png | Bin 0 -> 551 bytes .../vendor/firebug-lite/skin/xp/upHover.png | Bin 0 -> 526 bytes .../firebug-lite/skin/xp/warningIcon.gif | Bin 0 -> 357 bytes .../firebug-lite/skin/xp/warningIcon.png | Bin 0 -> 516 bytes .../firebug-lite/src/firebug-lite-debug.js | 31176 ++++++++++++++ .../lodash/vendor/json-js/json2.js | 519 + .../{ => lodash/vendor}/underscore/LICENSE | 2 +- .../lodash/vendor/underscore/test/arrays.js | 555 + .../lodash/vendor/underscore/test/chaining.js | 99 + .../vendor/underscore/test/collections.js | 896 + .../vendor/underscore/test/cross-document.js | 141 + .../vendor/underscore/test/functions.js | 728 + .../lodash/vendor/underscore/test/objects.js | 1102 + .../lodash/vendor/underscore/test/utility.js | 420 + .../vendor/underscore/underscore-min.js | 6 + .../lodash/vendor/underscore/underscore.js | 1620 + .../sass-bootstrap/.bower.json | 2 +- app/bower_components/underscore/.bower.json | 33 - app/bower_components/underscore/.gitignore | 2 - app/bower_components/underscore/README.md | 22 - app/bower_components/underscore/bower.json | 8 - .../underscore/component.json | 10 - app/bower_components/underscore/package.json | 27 - app/bower_components/underscore/underscore.js | 1343 - app/fonts/fontawesome-webfont.woff2 | 1 + app/index.html | 2 +- .../dialogs/outsideOfCommunityDialog.js | 10 +- bower.json | 46 +- package.json | 66 +- server/app.js | 2 +- server/libs.js | 2 +- server/main.js | 2 +- 454 files changed, 221168 insertions(+), 36622 deletions(-) create mode 100644 app/bower_components/angular-bootstrap/.gitignore create mode 100644 app/bower_components/angular-bootstrap/.npmignore create mode 100644 app/bower_components/angular-bootstrap/README.md create mode 100644 app/bower_components/angular-bootstrap/index.js create mode 100644 app/bower_components/angular-bootstrap/package.json create mode 100644 app/bower_components/angular-bootstrap/ui-bootstrap-csp.css create mode 100644 app/bower_components/angular-leaflet-directive/bower.json create mode 100644 app/bower_components/angular-leaflet-directive/coffeelint.json create mode 100644 app/bower_components/angular-leaflet-directive/dist/angular-leaflet-directive.no-header.js create mode 100644 app/bower_components/angular-leaflet-directive/generate-examples.js create mode 100644 app/bower_components/angular-leaflet-directive/grunt/aliases.yaml create mode 100644 app/bower_components/angular-leaflet-directive/grunt/clean.json create mode 100644 app/bower_components/angular-leaflet-directive/grunt/concat.json create mode 100644 app/bower_components/angular-leaflet-directive/grunt/connect.json create mode 100644 app/bower_components/angular-leaflet-directive/grunt/jscs.json create mode 100644 app/bower_components/angular-leaflet-directive/grunt/jshint.json create mode 100644 app/bower_components/angular-leaflet-directive/grunt/karma.json create mode 100644 app/bower_components/angular-leaflet-directive/grunt/ngAnnotate.json create mode 100644 app/bower_components/angular-leaflet-directive/grunt/open.json create mode 100644 app/bower_components/angular-leaflet-directive/grunt/protractor.json create mode 100644 app/bower_components/angular-leaflet-directive/grunt/shell.json create mode 100644 app/bower_components/angular-leaflet-directive/grunt/uglify.json create mode 100644 app/bower_components/angular-leaflet-directive/grunt/watch.json create mode 100644 app/bower_components/angular-mocks/ngAnimateMock.js create mode 100644 app/bower_components/angular-mocks/ngMock.js create mode 100644 app/bower_components/angular-mocks/ngMockE2E.js create mode 100644 app/bower_components/angular-mocks/package.json create mode 100644 app/bower_components/angular-route/index.js create mode 100644 app/bower_components/angular-route/package.json create mode 100644 app/bower_components/angular-sanitize/index.js create mode 100644 app/bower_components/angular-sanitize/package.json create mode 100644 app/bower_components/angular-scenario/package.json create mode 100644 app/bower_components/angular/index.js create mode 100644 app/bower_components/angular/package.json create mode 100644 app/bower_components/bootstrap-sass-official/assets/fonts/bootstrap/glyphicons-halflings-regular.woff2 create mode 100644 app/bower_components/bootstrap-sass-official/assets/javascripts/bootstrap.min.js rename app/bower_components/bootstrap-sass-official/assets/stylesheets/{bootstrap.scss => _bootstrap.scss} (88%) delete mode 100644 app/bower_components/bootstrap-sass-official/assets/stylesheets/bootstrap/bootstrap.scss create mode 100644 app/bower_components/bootstrap-sass-official/assets/stylesheets/bootstrap/mixins/_reset-text.scss create mode 100644 app/bower_components/es5-shim/Makefile create mode 100644 app/bower_components/font-awesome/HELP-US-OUT.txt create mode 100644 app/bower_components/font-awesome/bower.json create mode 100644 app/bower_components/font-awesome/css/font-awesome.css.map mode change 100755 => 100644 app/bower_components/font-awesome/fonts/fontawesome-webfont.eot mode change 100755 => 100644 app/bower_components/font-awesome/fonts/fontawesome-webfont.svg mode change 100755 => 100644 app/bower_components/font-awesome/fonts/fontawesome-webfont.ttf mode change 100755 => 100644 app/bower_components/font-awesome/fonts/fontawesome-webfont.woff create mode 100644 app/bower_components/font-awesome/fonts/fontawesome-webfont.woff2 create mode 100644 app/bower_components/font-awesome/less/animated.less delete mode 100644 app/bower_components/font-awesome/less/extras.less create mode 100644 app/bower_components/font-awesome/less/screen-reader.less delete mode 100644 app/bower_components/font-awesome/less/spinning.less create mode 100644 app/bower_components/font-awesome/scss/_animated.scss delete mode 100644 app/bower_components/font-awesome/scss/_extras.scss create mode 100644 app/bower_components/font-awesome/scss/_screen-reader.scss delete mode 100644 app/bower_components/font-awesome/scss/_spinning.scss create mode 100644 app/bower_components/geolib/dist/geolib.isPointInsideRobust.js create mode 100644 app/bower_components/geolib/dist/geolib.isPointInsideRobust.min.js create mode 100644 app/bower_components/geolib/meteor/README.md create mode 100644 app/bower_components/geolib/meteor/export.js create mode 100644 app/bower_components/geolib/meteor/package.js create mode 100644 app/bower_components/geolib/meteor/test.js create mode 100644 app/bower_components/geolib/src/geolib.isPointInsideRobust.js create mode 100644 app/bower_components/jquery/src/event/ajax.js create mode 100644 app/bower_components/leaflet/.bower.json create mode 100644 app/bower_components/leaflet/CHANGELOG.md create mode 100644 app/bower_components/leaflet/CONTRIBUTING.md create mode 100644 app/bower_components/leaflet/Jakefile.js create mode 100644 app/bower_components/leaflet/LICENSE create mode 100644 app/bower_components/leaflet/PLUGIN-GUIDE.md create mode 100644 app/bower_components/leaflet/README.md create mode 100644 app/bower_components/leaflet/bower.json create mode 100644 app/bower_components/leaflet/component.json create mode 100644 app/bower_components/leaflet/dist/images/layers-2x.png create mode 100644 app/bower_components/leaflet/dist/images/layers.png create mode 100644 app/bower_components/leaflet/dist/images/marker-icon-2x.png create mode 100644 app/bower_components/leaflet/dist/images/marker-icon.png create mode 100644 app/bower_components/leaflet/dist/images/marker-shadow.png create mode 100644 app/bower_components/leaflet/dist/leaflet-src.js create mode 100644 app/bower_components/leaflet/dist/leaflet.css create mode 100644 app/bower_components/leaflet/dist/leaflet.js create mode 100644 app/bower_components/leaflet/package.json create mode 100644 app/bower_components/lodash/.bower.json rename app/bower_components/{underscore => lodash}/.editorconfig (92%) create mode 100644 app/bower_components/lodash/.gitattributes create mode 100644 app/bower_components/lodash/.github/CONTRIBUTING.md create mode 100644 app/bower_components/lodash/.gitignore create mode 100644 app/bower_components/lodash/.jscsrc create mode 100644 app/bower_components/lodash/.markdown-doctest-setup.js create mode 100644 app/bower_components/lodash/.travis.yml create mode 100644 app/bower_components/lodash/LICENSE create mode 100644 app/bower_components/lodash/README.md create mode 100644 app/bower_components/lodash/dist/lodash.core.js create mode 100644 app/bower_components/lodash/dist/lodash.core.min.js create mode 100644 app/bower_components/lodash/dist/lodash.fp.js create mode 100644 app/bower_components/lodash/dist/lodash.fp.min.js create mode 100644 app/bower_components/lodash/dist/lodash.js create mode 100644 app/bower_components/lodash/dist/lodash.min.js create mode 100644 app/bower_components/lodash/dist/mapping.fp.js create mode 100644 app/bower_components/lodash/doc/README.md create mode 100644 app/bower_components/lodash/fp/_baseConvert.js create mode 100644 app/bower_components/lodash/fp/_convertBrowser.js create mode 100644 app/bower_components/lodash/fp/_mapping.js create mode 100644 app/bower_components/lodash/fp/placeholder.js create mode 100644 app/bower_components/lodash/lib/common/file.js create mode 100644 app/bower_components/lodash/lib/common/mapping.js create mode 100644 app/bower_components/lodash/lib/common/minify.js create mode 100644 app/bower_components/lodash/lib/common/uglify.options.js create mode 100644 app/bower_components/lodash/lib/common/util.js create mode 100644 app/bower_components/lodash/lib/fp/build-dist.js create mode 100644 app/bower_components/lodash/lib/fp/build-doc.js create mode 100644 app/bower_components/lodash/lib/fp/build-modules.js create mode 100644 app/bower_components/lodash/lib/fp/template/doc/wiki.jst create mode 100644 app/bower_components/lodash/lib/fp/template/modules/_falseOptions.jst create mode 100644 app/bower_components/lodash/lib/fp/template/modules/_util.jst create mode 100644 app/bower_components/lodash/lib/fp/template/modules/alias.jst create mode 100644 app/bower_components/lodash/lib/fp/template/modules/category.jst create mode 100644 app/bower_components/lodash/lib/fp/template/modules/convert.jst create mode 100644 app/bower_components/lodash/lib/fp/template/modules/fp.jst create mode 100644 app/bower_components/lodash/lib/fp/template/modules/module.jst create mode 100644 app/bower_components/lodash/lib/fp/template/modules/thru.jst create mode 100644 app/bower_components/lodash/lib/main/build-dist.js create mode 100644 app/bower_components/lodash/lib/main/build-doc.js create mode 100644 app/bower_components/lodash/lib/main/build-modules.js create mode 100644 app/bower_components/lodash/lodash.js create mode 100644 app/bower_components/lodash/package.json create mode 100644 app/bower_components/lodash/perf/asset/perf-ui.js create mode 100644 app/bower_components/lodash/perf/index.html create mode 100644 app/bower_components/lodash/perf/perf.js create mode 100644 app/bower_components/lodash/test/asset/test-ui.js create mode 100644 app/bower_components/lodash/test/asset/worker.js create mode 100644 app/bower_components/lodash/test/backbone.html create mode 100644 app/bower_components/lodash/test/fp.html create mode 100644 app/bower_components/lodash/test/index.html create mode 100644 app/bower_components/lodash/test/remove.js create mode 100644 app/bower_components/lodash/test/saucelabs.js create mode 100644 app/bower_components/lodash/test/test-fp.js create mode 100644 app/bower_components/lodash/test/test.js create mode 100644 app/bower_components/lodash/test/underscore.html create mode 100644 app/bower_components/lodash/vendor/backbone/LICENSE create mode 100644 app/bower_components/lodash/vendor/backbone/backbone.js create mode 100644 app/bower_components/lodash/vendor/backbone/test/collection.js create mode 100644 app/bower_components/lodash/vendor/backbone/test/events.js create mode 100644 app/bower_components/lodash/vendor/backbone/test/model.js create mode 100644 app/bower_components/lodash/vendor/backbone/test/noconflict.js create mode 100644 app/bower_components/lodash/vendor/backbone/test/router.js create mode 100644 app/bower_components/lodash/vendor/backbone/test/setup/dom-setup.js create mode 100644 app/bower_components/lodash/vendor/backbone/test/setup/environment.js create mode 100644 app/bower_components/lodash/vendor/backbone/test/sync.js create mode 100644 app/bower_components/lodash/vendor/backbone/test/view.js create mode 100644 app/bower_components/lodash/vendor/firebug-lite/license.txt create mode 100644 app/bower_components/lodash/vendor/firebug-lite/skin/xp/blank.gif create mode 100644 app/bower_components/lodash/vendor/firebug-lite/skin/xp/buttonBg.png create mode 100644 app/bower_components/lodash/vendor/firebug-lite/skin/xp/buttonBgHover.png create mode 100644 app/bower_components/lodash/vendor/firebug-lite/skin/xp/debugger.css create mode 100644 app/bower_components/lodash/vendor/firebug-lite/skin/xp/detach.png create mode 100644 app/bower_components/lodash/vendor/firebug-lite/skin/xp/detachHover.png create mode 100644 app/bower_components/lodash/vendor/firebug-lite/skin/xp/disable.gif create mode 100644 app/bower_components/lodash/vendor/firebug-lite/skin/xp/disable.png create mode 100644 app/bower_components/lodash/vendor/firebug-lite/skin/xp/disableHover.gif create mode 100644 app/bower_components/lodash/vendor/firebug-lite/skin/xp/disableHover.png create mode 100644 app/bower_components/lodash/vendor/firebug-lite/skin/xp/down.png create mode 100644 app/bower_components/lodash/vendor/firebug-lite/skin/xp/downActive.png create mode 100644 app/bower_components/lodash/vendor/firebug-lite/skin/xp/downHover.png create mode 100644 app/bower_components/lodash/vendor/firebug-lite/skin/xp/errorIcon-sm.png create mode 100644 app/bower_components/lodash/vendor/firebug-lite/skin/xp/errorIcon.gif create mode 100644 app/bower_components/lodash/vendor/firebug-lite/skin/xp/errorIcon.png create mode 100644 app/bower_components/lodash/vendor/firebug-lite/skin/xp/firebug-1.3a2.css create mode 100644 app/bower_components/lodash/vendor/firebug-lite/skin/xp/firebug.IE6.css create mode 100644 app/bower_components/lodash/vendor/firebug-lite/skin/xp/firebug.css create mode 100644 app/bower_components/lodash/vendor/firebug-lite/skin/xp/firebug.html create mode 100644 app/bower_components/lodash/vendor/firebug-lite/skin/xp/firebug.png create mode 100644 app/bower_components/lodash/vendor/firebug-lite/skin/xp/group.gif create mode 100644 app/bower_components/lodash/vendor/firebug-lite/skin/xp/html.css create mode 100644 app/bower_components/lodash/vendor/firebug-lite/skin/xp/infoIcon.gif create mode 100644 app/bower_components/lodash/vendor/firebug-lite/skin/xp/infoIcon.png create mode 100644 app/bower_components/lodash/vendor/firebug-lite/skin/xp/loading_16.gif create mode 100644 app/bower_components/lodash/vendor/firebug-lite/skin/xp/min.png create mode 100644 app/bower_components/lodash/vendor/firebug-lite/skin/xp/minHover.png create mode 100644 app/bower_components/lodash/vendor/firebug-lite/skin/xp/off.png create mode 100644 app/bower_components/lodash/vendor/firebug-lite/skin/xp/offHover.png create mode 100644 app/bower_components/lodash/vendor/firebug-lite/skin/xp/pixel_transparent.gif create mode 100644 app/bower_components/lodash/vendor/firebug-lite/skin/xp/roundCorner.svg create mode 100644 app/bower_components/lodash/vendor/firebug-lite/skin/xp/search.gif create mode 100644 app/bower_components/lodash/vendor/firebug-lite/skin/xp/search.png create mode 100644 app/bower_components/lodash/vendor/firebug-lite/skin/xp/shadow.gif create mode 100644 app/bower_components/lodash/vendor/firebug-lite/skin/xp/shadow2.gif create mode 100644 app/bower_components/lodash/vendor/firebug-lite/skin/xp/shadowAlpha.png create mode 100644 app/bower_components/lodash/vendor/firebug-lite/skin/xp/sprite.png create mode 100644 app/bower_components/lodash/vendor/firebug-lite/skin/xp/tabHoverLeft.png create mode 100644 app/bower_components/lodash/vendor/firebug-lite/skin/xp/tabHoverMid.png create mode 100644 app/bower_components/lodash/vendor/firebug-lite/skin/xp/tabHoverRight.png create mode 100644 app/bower_components/lodash/vendor/firebug-lite/skin/xp/tabLeft.png create mode 100644 app/bower_components/lodash/vendor/firebug-lite/skin/xp/tabMenuCheckbox.png create mode 100644 app/bower_components/lodash/vendor/firebug-lite/skin/xp/tabMenuPin.png create mode 100644 app/bower_components/lodash/vendor/firebug-lite/skin/xp/tabMenuRadio.png create mode 100644 app/bower_components/lodash/vendor/firebug-lite/skin/xp/tabMenuTarget.png create mode 100644 app/bower_components/lodash/vendor/firebug-lite/skin/xp/tabMenuTargetHover.png create mode 100644 app/bower_components/lodash/vendor/firebug-lite/skin/xp/tabMid.png create mode 100644 app/bower_components/lodash/vendor/firebug-lite/skin/xp/tabRight.png create mode 100644 app/bower_components/lodash/vendor/firebug-lite/skin/xp/textEditorBorders.gif create mode 100644 app/bower_components/lodash/vendor/firebug-lite/skin/xp/textEditorBorders.png create mode 100644 app/bower_components/lodash/vendor/firebug-lite/skin/xp/textEditorCorners.gif create mode 100644 app/bower_components/lodash/vendor/firebug-lite/skin/xp/textEditorCorners.png create mode 100644 app/bower_components/lodash/vendor/firebug-lite/skin/xp/titlebarMid.png create mode 100644 app/bower_components/lodash/vendor/firebug-lite/skin/xp/toolbarMid.png create mode 100644 app/bower_components/lodash/vendor/firebug-lite/skin/xp/tree_close.gif create mode 100644 app/bower_components/lodash/vendor/firebug-lite/skin/xp/tree_open.gif create mode 100644 app/bower_components/lodash/vendor/firebug-lite/skin/xp/twistyClosed.png create mode 100644 app/bower_components/lodash/vendor/firebug-lite/skin/xp/twistyOpen.png create mode 100644 app/bower_components/lodash/vendor/firebug-lite/skin/xp/up.png create mode 100644 app/bower_components/lodash/vendor/firebug-lite/skin/xp/upActive.png create mode 100644 app/bower_components/lodash/vendor/firebug-lite/skin/xp/upHover.png create mode 100644 app/bower_components/lodash/vendor/firebug-lite/skin/xp/warningIcon.gif create mode 100644 app/bower_components/lodash/vendor/firebug-lite/skin/xp/warningIcon.png create mode 100644 app/bower_components/lodash/vendor/firebug-lite/src/firebug-lite-debug.js create mode 100644 app/bower_components/lodash/vendor/json-js/json2.js rename app/bower_components/{ => lodash/vendor}/underscore/LICENSE (94%) create mode 100644 app/bower_components/lodash/vendor/underscore/test/arrays.js create mode 100644 app/bower_components/lodash/vendor/underscore/test/chaining.js create mode 100644 app/bower_components/lodash/vendor/underscore/test/collections.js create mode 100644 app/bower_components/lodash/vendor/underscore/test/cross-document.js create mode 100644 app/bower_components/lodash/vendor/underscore/test/functions.js create mode 100644 app/bower_components/lodash/vendor/underscore/test/objects.js create mode 100644 app/bower_components/lodash/vendor/underscore/test/utility.js create mode 100644 app/bower_components/lodash/vendor/underscore/underscore-min.js create mode 100644 app/bower_components/lodash/vendor/underscore/underscore.js delete mode 100644 app/bower_components/underscore/.bower.json delete mode 100644 app/bower_components/underscore/.gitignore delete mode 100644 app/bower_components/underscore/README.md delete mode 100644 app/bower_components/underscore/bower.json delete mode 100644 app/bower_components/underscore/component.json delete mode 100644 app/bower_components/underscore/package.json delete mode 100644 app/bower_components/underscore/underscore.js create mode 120000 app/fonts/fontawesome-webfont.woff2 diff --git a/Gruntfile.js b/Gruntfile.js index 4c55030..3704daa 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -1,4 +1,3 @@ -// Generated on 2014-05-12 using generator-angular 0.8.0 'use strict'; module.exports = function (grunt) { @@ -9,6 +8,8 @@ module.exports = function (grunt) { // Time how long tasks take. Can help when optimizing build times require('time-grunt')(grunt); + var serveStatic = require('serve-static'); + // Define the configuration for all the tasks grunt.initConfig({ @@ -23,7 +24,7 @@ module.exports = function (grunt) { watch: { bower: { files: ['bower.json'], - tasks: ['bowerInstall'] + tasks: ['wiredep'] }, js: { files: ['<%= yeoman.app %>/scripts/{,**/}*.js', 'shared/{,**/}*.js'], @@ -68,12 +69,12 @@ module.exports = function (grunt) { // Serve static files. options.base.forEach(function(base) { - middlewares.push(connect.static(base)); + middlewares.push(serveStatic(base)); }); // Make directory browse-able. var directory = options.directory || options.base[options.base.length - 1]; - middlewares.push(connect.directory(directory)); + middlewares.push(serveStatic(directory)); return middlewares; } @@ -152,9 +153,8 @@ module.exports = function (grunt) { } }, - // Automatically inject Bower components into the app - bowerInstall: { - sass: { + wiredep: { + task: { src: ['<%= yeoman.app %>/styles/{,*/}*.{scss,sass}'], ignorePath: '<%= yeoman.app %>/bower_components/' } @@ -302,10 +302,7 @@ module.exports = function (grunt) { } }, - // ngmin tries to make the code safe for minification automatically by - // using the Angular long form for dependency injection. It doesn't work on - // things like resolve or inject so those have to be done manually. - ngmin: { + ngAnnotate: { dist: { files: [ { @@ -406,7 +403,7 @@ module.exports = function (grunt) { return grunt.task.run([ 'clean:server', - 'bowerInstall', + 'wiredep', 'concurrent:server', 'autoprefixer', 'configureProxies', @@ -422,13 +419,13 @@ module.exports = function (grunt) { grunt.registerTask('build', [ 'clean:dist', - 'bowerInstall', + 'wiredep', 'html2js', 'useminPrepare', 'concurrent:dist', 'autoprefixer', 'concat', - 'ngmin', + 'ngAnnotate', 'copy:dist', 'cssmin', 'uglify', diff --git a/app/bower_components/angular-bootstrap/.bower.json b/app/bower_components/angular-bootstrap/.bower.json index ba4deca..bc80c23 100644 --- a/app/bower_components/angular-bootstrap/.bower.json +++ b/app/bower_components/angular-bootstrap/.bower.json @@ -3,22 +3,29 @@ "name": "https://github.com/angular-ui/bootstrap/graphs/contributors" }, "name": "angular-bootstrap", - "version": "0.11.0", + "keywords": [ + "angular", + "angular-ui", + "bootstrap" + ], + "license": "MIT", + "ignore": [], + "description": "Native AngularJS (Angular) directives for Bootstrap.", + "version": "1.3.2", "main": [ "./ui-bootstrap-tpls.js" ], "dependencies": { - "angular": ">=1" + "angular": ">=1.4.0" }, "homepage": "https://github.com/angular-ui/bootstrap-bower", - "_release": "0.11.0", + "_release": "1.3.2", "_resolution": { "type": "version", - "tag": "0.11.0", - "commit": "75b302f82c1a3b0647695a3dfeacab0a153ea8a0" + "tag": "1.3.2", + "commit": "77da362b0b86c0a86762b56d64aaec107889f31a" }, - "_source": "git://github.com/angular-ui/bootstrap-bower.git", - "_target": "~0.11.0", - "_originalSource": "angular-bootstrap", - "_direct": true + "_source": "https://github.com/angular-ui/bootstrap-bower.git", + "_target": "~1.3.2", + "_originalSource": "angular-bootstrap" } \ No newline at end of file diff --git a/app/bower_components/angular-bootstrap/.gitignore b/app/bower_components/angular-bootstrap/.gitignore new file mode 100644 index 0000000..496ee2c --- /dev/null +++ b/app/bower_components/angular-bootstrap/.gitignore @@ -0,0 +1 @@ +.DS_Store \ No newline at end of file diff --git a/app/bower_components/angular-bootstrap/.npmignore b/app/bower_components/angular-bootstrap/.npmignore new file mode 100644 index 0000000..d62f9b6 --- /dev/null +++ b/app/bower_components/angular-bootstrap/.npmignore @@ -0,0 +1 @@ +bower.json \ No newline at end of file diff --git a/app/bower_components/angular-bootstrap/README.md b/app/bower_components/angular-bootstrap/README.md new file mode 100644 index 0000000..9607c65 --- /dev/null +++ b/app/bower_components/angular-bootstrap/README.md @@ -0,0 +1,120 @@ +### UI Bootstrap - [AngularJS](http://angularjs.org/) directives specific to [Bootstrap](http://getbootstrap.com) + +[![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/angular-ui/bootstrap?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) +[![Build Status](https://secure.travis-ci.org/angular-ui/bootstrap.svg)](http://travis-ci.org/angular-ui/bootstrap) +[![devDependency Status](https://david-dm.org/angular-ui/bootstrap/dev-status.svg?branch=master)](https://david-dm.org/angular-ui/bootstrap#info=devDependencies) + +### Quick links +- [Demo](#demo) +- [Installation](#installation) + - [NPM](#install-with-npm) + - [Bower](#install-with-bower) + - [NuGet](#install-with-nuget) + - [Custom](#custom-build) + - [Manual](#manual-download) +- [Support](#support) + - [FAQ](#faq) + - [Supported browsers](#supported-browsers) + - [Need help?](#need-help) + - [Found a bug?](#found-a-bug) +- [Contributing to the project](#contributing-to-the-project) +- [Development, meeting minutes, roadmap and more.](#development-meeting-minutes-roadmap-and-more) + + +# Demo + +Do you want to see directives in action? Visit http://angular-ui.github.io/bootstrap/! + +# Installation + +Installation is easy as UI Bootstrap has minimal dependencies - only the AngularJS and Twitter Bootstrap's CSS are required. +Note: Since version 0.13.0, UI Bootstrap depends on [ngAnimate](https://docs.angularjs.org/api/ngAnimate) for transitions and animations, such as the accordion, carousel, etc. Include `ngAnimate` in the module dependencies for your app in order to enable animation. + +#### Install with NPM + +```sh +$ npm install angular-ui-bootstrap +``` + +This will install AngularJS and Bootstrap NPM packages. + +#### Install with Bower +```sh +$ bower install angular-bootstrap +``` + +Note: do not install 'angular-ui-bootstrap'. A separate repository - [bootstrap-bower](https://github.com/angular-ui/bootstrap-bower) - hosts the compiled javascript file and bower.json. + +#### Install with NuGet +To install AngularJS UI Bootstrap, run the following command in the Package Manager Console + +```sh +PM> Install-Package Angular.UI.Bootstrap +``` + +#### Custom build + +Head over to http://angular-ui.github.io/bootstrap/ and hit the *Custom build* button to create your own custom UI Bootstrap build, just the way you like it. + +#### Manual download + +After downloading dependencies (or better yet, referencing them from your favorite CDN) you need to download build version of this project. All the files and their purposes are described here: +https://github.com/angular-ui/bootstrap/tree/gh-pages#build-files +Don't worry, if you are not sure which file to take, opt for `ui-bootstrap-tpls-[version].min.js`. + +### Adding dependency to your project + +When you are done downloading all the dependencies and project files the only remaining part is to add dependencies on the `ui.bootstrap` AngularJS module: + +```js +angular.module('myModule', ['ui.bootstrap']); +``` + +If you're a Browserify or Webpack user, you can do: + +```js +var uibs = require('angular-ui-bootstrap'); + +angular.module('myModule', [uibs]); +``` + +# Support + +## FAQ + +https://github.com/angular-ui/bootstrap/wiki/FAQ + +## Supported browsers + +Directives from this repository are automatically tested with the following browsers: +* Chrome (stable and canary channel) +* Firefox +* IE 9 and 10 +* Opera +* Safari + +Modern mobile browsers should work without problems. + + +## Need help? +Need help using UI Bootstrap? + +* Live help in the IRC (`#angularjs` channel at the `freenode` network). Use this [webchat](https://webchat.freenode.net/) or your own IRC client. +* Ask a question in [StackOverflow](http://stackoverflow.com/) under the [angular-ui-bootstrap](http://stackoverflow.com/questions/tagged/angular-ui-bootstrap) tag. + +**Please do not create new issues in this repository to ask questions about using UI Bootstrap** + +## Found a bug? +Please take a look at [CONTRIBUTING.md](CONTRIBUTING.md#you-think-youve-found-a-bug) and submit your issue [here](https://github.com/angular-ui/bootstrap/issues/new). + + +---- + + +# Contributing to the project + +We are always looking for the quality contributions! Please check the [CONTRIBUTING.md](CONTRIBUTING.md) for the contribution guidelines. + +# Development, meeting minutes, roadmap and more. + +Head over to the [Wiki](https://github.com/angular-ui/bootstrap/wiki) for notes on development for UI Bootstrap, meeting minutes from the UI Bootstrap team, roadmap plans, project philosophy and more. diff --git a/app/bower_components/angular-bootstrap/bower.json b/app/bower_components/angular-bootstrap/bower.json index 6793432..181ab4e 100644 --- a/app/bower_components/angular-bootstrap/bower.json +++ b/app/bower_components/angular-bootstrap/bower.json @@ -3,9 +3,17 @@ "name": "https://github.com/angular-ui/bootstrap/graphs/contributors" }, "name": "angular-bootstrap", - "version": "0.11.0", + "keywords": [ + "angular", + "angular-ui", + "bootstrap" + ], + "license": "MIT", + "ignore": [], + "description": "Native AngularJS (Angular) directives for Bootstrap.", + "version": "1.3.2", "main": ["./ui-bootstrap-tpls.js"], "dependencies": { - "angular": ">=1" - } + "angular": ">=1.4.0" + } } diff --git a/app/bower_components/angular-bootstrap/index.js b/app/bower_components/angular-bootstrap/index.js new file mode 100644 index 0000000..a174f26 --- /dev/null +++ b/app/bower_components/angular-bootstrap/index.js @@ -0,0 +1,2 @@ +require('./ui-bootstrap-tpls'); +module.exports = 'ui.bootstrap'; diff --git a/app/bower_components/angular-bootstrap/package.json b/app/bower_components/angular-bootstrap/package.json new file mode 100644 index 0000000..7b8c92f --- /dev/null +++ b/app/bower_components/angular-bootstrap/package.json @@ -0,0 +1,23 @@ +{ + "name": "angular-ui-bootstrap", + "version": "1.3.2", + "description": "Bootstrap widgets for Angular", + "main": "index.js", + "homepage": "http://angular-ui.github.io/bootstrap/", + "repository": { + "type": "git", + "url": "https://github.com/angular-ui/bootstrap.git" + }, + "keywords": [ + "angular", + "bootstrap", + "angular-ui", + "components", + "client-side" + ], + "author": "https://github.com/angular-ui/bootstrap/graphs/contributors", + "peerDependencies": { + "angular": ">= 1.4.0-beta.0 || >= 1.5.0-beta.0" + }, + "license": "MIT" +} diff --git a/app/bower_components/angular-bootstrap/ui-bootstrap-csp.css b/app/bower_components/angular-bootstrap/ui-bootstrap-csp.css new file mode 100644 index 0000000..3b69cf6 --- /dev/null +++ b/app/bower_components/angular-bootstrap/ui-bootstrap-csp.css @@ -0,0 +1,115 @@ +/* Include this file in your html if you are using the CSP mode. */ + +.ng-animate.item:not(.left):not(.right) { + -webkit-transition: 0s ease-in-out left; + transition: 0s ease-in-out left +} +.uib-datepicker .uib-title { + width: 100%; +} + +.uib-day button, .uib-month button, .uib-year button { + min-width: 100%; +} + +.uib-left, .uib-right { + width: 100% +} + +.uib-position-measure { + display: block !important; + visibility: hidden !important; + position: absolute !important; + top: -9999px !important; + left: -9999px !important; +} + +.uib-position-scrollbar-measure { + position: absolute !important; + top: -9999px !important; + width: 50px !important; + height: 50px !important; + overflow: scroll !important; +} + +.uib-position-body-scrollbar-measure { + overflow: scroll !important; +} +.uib-datepicker-popup.dropdown-menu { + display: block; + float: none; + margin: 0; +} + +.uib-button-bar { + padding: 10px 9px 2px; +} + +[uib-tooltip-popup].tooltip.top-left > .tooltip-arrow, +[uib-tooltip-popup].tooltip.top-right > .tooltip-arrow, +[uib-tooltip-popup].tooltip.bottom-left > .tooltip-arrow, +[uib-tooltip-popup].tooltip.bottom-right > .tooltip-arrow, +[uib-tooltip-popup].tooltip.left-top > .tooltip-arrow, +[uib-tooltip-popup].tooltip.left-bottom > .tooltip-arrow, +[uib-tooltip-popup].tooltip.right-top > .tooltip-arrow, +[uib-tooltip-popup].tooltip.right-bottom > .tooltip-arrow, +[uib-tooltip-html-popup].tooltip.top-left > .tooltip-arrow, +[uib-tooltip-html-popup].tooltip.top-right > .tooltip-arrow, +[uib-tooltip-html-popup].tooltip.bottom-left > .tooltip-arrow, +[uib-tooltip-html-popup].tooltip.bottom-right > .tooltip-arrow, +[uib-tooltip-html-popup].tooltip.left-top > .tooltip-arrow, +[uib-tooltip-html-popup].tooltip.left-bottom > .tooltip-arrow, +[uib-tooltip-html-popup].tooltip.right-top > .tooltip-arrow, +[uib-tooltip-html-popup].tooltip.right-bottom > .tooltip-arrow, +[uib-tooltip-template-popup].tooltip.top-left > .tooltip-arrow, +[uib-tooltip-template-popup].tooltip.top-right > .tooltip-arrow, +[uib-tooltip-template-popup].tooltip.bottom-left > .tooltip-arrow, +[uib-tooltip-template-popup].tooltip.bottom-right > .tooltip-arrow, +[uib-tooltip-template-popup].tooltip.left-top > .tooltip-arrow, +[uib-tooltip-template-popup].tooltip.left-bottom > .tooltip-arrow, +[uib-tooltip-template-popup].tooltip.right-top > .tooltip-arrow, +[uib-tooltip-template-popup].tooltip.right-bottom > .tooltip-arrow, +[uib-popover-popup].popover.top-left > .arrow, +[uib-popover-popup].popover.top-right > .arrow, +[uib-popover-popup].popover.bottom-left > .arrow, +[uib-popover-popup].popover.bottom-right > .arrow, +[uib-popover-popup].popover.left-top > .arrow, +[uib-popover-popup].popover.left-bottom > .arrow, +[uib-popover-popup].popover.right-top > .arrow, +[uib-popover-popup].popover.right-bottom > .arrow, +[uib-popover-html-popup].popover.top-left > .arrow, +[uib-popover-html-popup].popover.top-right > .arrow, +[uib-popover-html-popup].popover.bottom-left > .arrow, +[uib-popover-html-popup].popover.bottom-right > .arrow, +[uib-popover-html-popup].popover.left-top > .arrow, +[uib-popover-html-popup].popover.left-bottom > .arrow, +[uib-popover-html-popup].popover.right-top > .arrow, +[uib-popover-html-popup].popover.right-bottom > .arrow, +[uib-popover-template-popup].popover.top-left > .arrow, +[uib-popover-template-popup].popover.top-right > .arrow, +[uib-popover-template-popup].popover.bottom-left > .arrow, +[uib-popover-template-popup].popover.bottom-right > .arrow, +[uib-popover-template-popup].popover.left-top > .arrow, +[uib-popover-template-popup].popover.left-bottom > .arrow, +[uib-popover-template-popup].popover.right-top > .arrow, +[uib-popover-template-popup].popover.right-bottom > .arrow { + top: auto; + bottom: auto; + left: auto; + right: auto; + margin: 0; +} + +[uib-popover-popup].popover, +[uib-popover-html-popup].popover, +[uib-popover-template-popup].popover { + display: block !important; +} + +.uib-time input { + width: 50px; +} + +[uib-typeahead-popup].dropdown-menu { + display: block; +} diff --git a/app/bower_components/angular-bootstrap/ui-bootstrap-tpls.js b/app/bower_components/angular-bootstrap/ui-bootstrap-tpls.js index bcca1cd..5b99358 100644 --- a/app/bower_components/angular-bootstrap/ui-bootstrap-tpls.js +++ b/app/bower_components/angular-bootstrap/ui-bootstrap-tpls.js @@ -2,160 +2,102 @@ * angular-ui-bootstrap * http://angular-ui.github.io/bootstrap/ - * Version: 0.11.0 - 2014-05-01 + * Version: 1.3.2 - 2016-04-14 * License: MIT - */ -angular.module("ui.bootstrap", ["ui.bootstrap.tpls", "ui.bootstrap.transition","ui.bootstrap.collapse","ui.bootstrap.accordion","ui.bootstrap.alert","ui.bootstrap.bindHtml","ui.bootstrap.buttons","ui.bootstrap.carousel","ui.bootstrap.dateparser","ui.bootstrap.position","ui.bootstrap.datepicker","ui.bootstrap.dropdown","ui.bootstrap.modal","ui.bootstrap.pagination","ui.bootstrap.tooltip","ui.bootstrap.popover","ui.bootstrap.progressbar","ui.bootstrap.rating","ui.bootstrap.tabs","ui.bootstrap.timepicker","ui.bootstrap.typeahead"]); -angular.module("ui.bootstrap.tpls", ["template/accordion/accordion-group.html","template/accordion/accordion.html","template/alert/alert.html","template/carousel/carousel.html","template/carousel/slide.html","template/datepicker/datepicker.html","template/datepicker/day.html","template/datepicker/month.html","template/datepicker/popup.html","template/datepicker/year.html","template/modal/backdrop.html","template/modal/window.html","template/pagination/pager.html","template/pagination/pagination.html","template/tooltip/tooltip-html-unsafe-popup.html","template/tooltip/tooltip-popup.html","template/popover/popover.html","template/progressbar/bar.html","template/progressbar/progress.html","template/progressbar/progressbar.html","template/rating/rating.html","template/tabs/tab.html","template/tabs/tabset.html","template/timepicker/timepicker.html","template/typeahead/typeahead-match.html","template/typeahead/typeahead-popup.html"]); -angular.module('ui.bootstrap.transition', []) - -/** - * $transition service provides a consistent interface to trigger CSS 3 transitions and to be informed when they complete. - * @param {DOMElement} element The DOMElement that will be animated. - * @param {string|object|function} trigger The thing that will cause the transition to start: - * - As a string, it represents the css class to be added to the element. - * - As an object, it represents a hash of style attributes to be applied to the element. - * - As a function, it represents a function to be called that will cause the transition to occur. - * @return {Promise} A promise that is resolved when the transition finishes. - */ -.factory('$transition', ['$q', '$timeout', '$rootScope', function($q, $timeout, $rootScope) { - - var $transition = function(element, trigger, options) { - options = options || {}; - var deferred = $q.defer(); - var endEventName = $transition[options.animation ? 'animationEndEventName' : 'transitionEndEventName']; - - var transitionEndHandler = function(event) { - $rootScope.$apply(function() { - element.unbind(endEventName, transitionEndHandler); - deferred.resolve(element); - }); - }; - - if (endEventName) { - element.bind(endEventName, transitionEndHandler); - } - - // Wrap in a timeout to allow the browser time to update the DOM before the transition is to occur - $timeout(function() { - if ( angular.isString(trigger) ) { - element.addClass(trigger); - } else if ( angular.isFunction(trigger) ) { - trigger(element); - } else if ( angular.isObject(trigger) ) { - element.css(trigger); - } - //If browser does not support transitions, instantly resolve - if ( !endEventName ) { - deferred.resolve(element); - } - }); - - // Add our custom cancel function to the promise that is returned - // We can call this if we are about to run a new transition, which we know will prevent this transition from ending, - // i.e. it will therefore never raise a transitionEnd event for that transition - deferred.promise.cancel = function() { - if ( endEventName ) { - element.unbind(endEventName, transitionEndHandler); - } - deferred.reject('Transition cancelled'); - }; - - return deferred.promise; - }; - - // Work out the name of the transitionEnd event - var transElement = document.createElement('trans'); - var transitionEndEventNames = { - 'WebkitTransition': 'webkitTransitionEnd', - 'MozTransition': 'transitionend', - 'OTransition': 'oTransitionEnd', - 'transition': 'transitionend' - }; - var animationEndEventNames = { - 'WebkitTransition': 'webkitAnimationEnd', - 'MozTransition': 'animationend', - 'OTransition': 'oAnimationEnd', - 'transition': 'animationend' - }; - function findEndEventName(endEventNames) { - for (var name in endEventNames){ - if (transElement.style[name] !== undefined) { - return endEventNames[name]; - } - } - } - $transition.transitionEndEventName = findEndEventName(transitionEndEventNames); - $transition.animationEndEventName = findEndEventName(animationEndEventNames); - return $transition; -}]); - -angular.module('ui.bootstrap.collapse', ['ui.bootstrap.transition']) - - .directive('collapse', ['$transition', function ($transition) { + */angular.module("ui.bootstrap", ["ui.bootstrap.tpls", "ui.bootstrap.collapse","ui.bootstrap.accordion","ui.bootstrap.alert","ui.bootstrap.buttons","ui.bootstrap.carousel","ui.bootstrap.dateparser","ui.bootstrap.isClass","ui.bootstrap.datepicker","ui.bootstrap.position","ui.bootstrap.datepickerPopup","ui.bootstrap.debounce","ui.bootstrap.dropdown","ui.bootstrap.stackedMap","ui.bootstrap.modal","ui.bootstrap.paging","ui.bootstrap.pager","ui.bootstrap.pagination","ui.bootstrap.tooltip","ui.bootstrap.popover","ui.bootstrap.progressbar","ui.bootstrap.rating","ui.bootstrap.tabs","ui.bootstrap.timepicker","ui.bootstrap.typeahead"]); +angular.module("ui.bootstrap.tpls", ["uib/template/accordion/accordion-group.html","uib/template/accordion/accordion.html","uib/template/alert/alert.html","uib/template/carousel/carousel.html","uib/template/carousel/slide.html","uib/template/datepicker/datepicker.html","uib/template/datepicker/day.html","uib/template/datepicker/month.html","uib/template/datepicker/year.html","uib/template/datepickerPopup/popup.html","uib/template/modal/backdrop.html","uib/template/modal/window.html","uib/template/pager/pager.html","uib/template/pagination/pagination.html","uib/template/tooltip/tooltip-html-popup.html","uib/template/tooltip/tooltip-popup.html","uib/template/tooltip/tooltip-template-popup.html","uib/template/popover/popover-html.html","uib/template/popover/popover-template.html","uib/template/popover/popover.html","uib/template/progressbar/bar.html","uib/template/progressbar/progress.html","uib/template/progressbar/progressbar.html","uib/template/rating/rating.html","uib/template/tabs/tab.html","uib/template/tabs/tabset.html","uib/template/timepicker/timepicker.html","uib/template/typeahead/typeahead-match.html","uib/template/typeahead/typeahead-popup.html"]); +angular.module('ui.bootstrap.collapse', []) + .directive('uibCollapse', ['$animate', '$q', '$parse', '$injector', function($animate, $q, $parse, $injector) { + var $animateCss = $injector.has('$animateCss') ? $injector.get('$animateCss') : null; return { - link: function (scope, element, attrs) { + link: function(scope, element, attrs) { + var expandingExpr = $parse(attrs.expanding), + expandedExpr = $parse(attrs.expanded), + collapsingExpr = $parse(attrs.collapsing), + collapsedExpr = $parse(attrs.collapsed); - var initialAnimSkip = true; - var currentTransition; - - function doTransition(change) { - var newTransition = $transition(element, change); - if (currentTransition) { - currentTransition.cancel(); - } - currentTransition = newTransition; - newTransition.then(newTransitionDone, newTransitionDone); - return newTransition; - - function newTransitionDone() { - // Make sure it's this transition, otherwise, leave it alone. - if (currentTransition === newTransition) { - currentTransition = undefined; - } - } + if (!scope.$eval(attrs.uibCollapse)) { + element.addClass('in') + .addClass('collapse') + .attr('aria-expanded', true) + .attr('aria-hidden', false) + .css({height: 'auto'}); } function expand() { - if (initialAnimSkip) { - initialAnimSkip = false; - expandDone(); - } else { - element.removeClass('collapse').addClass('collapsing'); - doTransition({ height: element[0].scrollHeight + 'px' }).then(expandDone); + if (element.hasClass('collapse') && element.hasClass('in')) { + return; } + + $q.resolve(expandingExpr(scope)) + .then(function() { + element.removeClass('collapse') + .addClass('collapsing') + .attr('aria-expanded', true) + .attr('aria-hidden', false); + + if ($animateCss) { + $animateCss(element, { + addClass: 'in', + easing: 'ease', + to: { height: element[0].scrollHeight + 'px' } + }).start()['finally'](expandDone); + } else { + $animate.addClass(element, 'in', { + to: { height: element[0].scrollHeight + 'px' } + }).then(expandDone); + } + }); } function expandDone() { - element.removeClass('collapsing'); - element.addClass('collapse in'); - element.css({height: 'auto'}); + element.removeClass('collapsing') + .addClass('collapse') + .css({height: 'auto'}); + expandedExpr(scope); } function collapse() { - if (initialAnimSkip) { - initialAnimSkip = false; - collapseDone(); - element.css({height: 0}); - } else { - // CSS transitions don't work with height: auto, so we have to manually change the height to a specific value - element.css({ height: element[0].scrollHeight + 'px' }); - //trigger reflow so a browser realizes that height was updated from auto to a specific value - var x = element[0].offsetWidth; - - element.removeClass('collapse in').addClass('collapsing'); - - doTransition({ height: 0 }).then(collapseDone); + if (!element.hasClass('collapse') && !element.hasClass('in')) { + return collapseDone(); } + + $q.resolve(collapsingExpr(scope)) + .then(function() { + element + // IMPORTANT: The height must be set before adding "collapsing" class. + // Otherwise, the browser attempts to animate from height 0 (in + // collapsing class) to the given height here. + .css({height: element[0].scrollHeight + 'px'}) + // initially all panel collapse have the collapse class, this removal + // prevents the animation from jumping to collapsed state + .removeClass('collapse') + .addClass('collapsing') + .attr('aria-expanded', false) + .attr('aria-hidden', true); + + if ($animateCss) { + $animateCss(element, { + removeClass: 'in', + to: {height: '0'} + }).start()['finally'](collapseDone); + } else { + $animate.removeClass(element, 'in', { + to: {height: '0'} + }).then(collapseDone); + } + }); } function collapseDone() { - element.removeClass('collapsing'); - element.addClass('collapse'); + element.css({height: '0'}); // Required so that collapse works when animation is disabled + element.removeClass('collapsing') + .addClass('collapse'); + collapsedExpr(scope); } - scope.$watch(attrs.collapse, function (shouldCollapse) { + scope.$watch(attrs.uibCollapse, function(shouldCollapse) { if (shouldCollapse) { collapse(); } else { @@ -168,21 +110,21 @@ angular.module('ui.bootstrap.collapse', ['ui.bootstrap.transition']) angular.module('ui.bootstrap.accordion', ['ui.bootstrap.collapse']) -.constant('accordionConfig', { +.constant('uibAccordionConfig', { closeOthers: true }) -.controller('AccordionController', ['$scope', '$attrs', 'accordionConfig', function ($scope, $attrs, accordionConfig) { - +.controller('UibAccordionController', ['$scope', '$attrs', 'uibAccordionConfig', function($scope, $attrs, accordionConfig) { // This array keeps track of the accordion groups this.groups = []; // Ensure that all the groups in this accordion are closed, unless close-others explicitly says not to this.closeOthers = function(openGroup) { - var closeOthers = angular.isDefined($attrs.closeOthers) ? $scope.$eval($attrs.closeOthers) : accordionConfig.closeOthers; - if ( closeOthers ) { - angular.forEach(this.groups, function (group) { - if ( group !== openGroup ) { + var closeOthers = angular.isDefined($attrs.closeOthers) ? + $scope.$eval($attrs.closeOthers) : accordionConfig.closeOthers; + if (closeOthers) { + angular.forEach(this.groups, function(group) { + if (group !== openGroup) { group.isOpen = false; } }); @@ -194,7 +136,7 @@ angular.module('ui.bootstrap.accordion', ['ui.bootstrap.collapse']) var that = this; this.groups.push(groupScope); - groupScope.$on('$destroy', function (event) { + groupScope.$on('$destroy', function(event) { that.removeGroup(groupScope); }); }; @@ -202,35 +144,37 @@ angular.module('ui.bootstrap.accordion', ['ui.bootstrap.collapse']) // This is called from the accordion-group directive when to remove itself this.removeGroup = function(group) { var index = this.groups.indexOf(group); - if ( index !== -1 ) { + if (index !== -1) { this.groups.splice(index, 1); } }; - }]) // The accordion directive simply sets up the directive controller // and adds an accordion CSS class to itself element. -.directive('accordion', function () { +.directive('uibAccordion', function() { return { - restrict:'EA', - controller:'AccordionController', + controller: 'UibAccordionController', + controllerAs: 'accordion', transclude: true, - replace: false, - templateUrl: 'template/accordion/accordion.html' + templateUrl: function(element, attrs) { + return attrs.templateUrl || 'uib/template/accordion/accordion.html'; + } }; }) // The accordion-group directive indicates a block of html that will expand and collapse in an accordion -.directive('accordionGroup', function() { +.directive('uibAccordionGroup', function() { return { - require:'^accordion', // We need this directive to be inside an accordion - restrict:'EA', - transclude:true, // It transcludes the contents of the directive into the template + require: '^uibAccordion', // We need this directive to be inside an accordion + transclude: true, // It transcludes the contents of the directive into the template replace: true, // The element containing the directive will be replaced with the template - templateUrl:'template/accordion/accordion-group.html', + templateUrl: function(element, attrs) { + return attrs.templateUrl || 'uib/template/accordion/accordion-group.html'; + }, scope: { heading: '@', // Interpolate the heading attribute onto this scope + panelClass: '@?', // Ditto with panelClass isOpen: '=?', isDisabled: '=?' }, @@ -242,55 +186,57 @@ angular.module('ui.bootstrap.accordion', ['ui.bootstrap.collapse']) link: function(scope, element, attrs, accordionCtrl) { accordionCtrl.addGroup(scope); + scope.openClass = attrs.openClass || 'panel-open'; + scope.panelClass = attrs.panelClass || 'panel-default'; scope.$watch('isOpen', function(value) { - if ( value ) { + element.toggleClass(scope.openClass, !!value); + if (value) { accordionCtrl.closeOthers(scope); } }); - scope.toggleOpen = function() { - if ( !scope.isDisabled ) { - scope.isOpen = !scope.isOpen; + scope.toggleOpen = function($event) { + if (!scope.isDisabled) { + if (!$event || $event.which === 32) { + scope.isOpen = !scope.isOpen; + } } }; + + var id = 'accordiongroup-' + scope.$id + '-' + Math.floor(Math.random() * 10000); + scope.headingId = id + '-tab'; + scope.panelId = id + '-panel'; } }; }) // Use accordion-heading below an accordion-group to provide a heading containing HTML -// -// Heading containing HTML - -// -.directive('accordionHeading', function() { +.directive('uibAccordionHeading', function() { return { - restrict: 'EA', transclude: true, // Grab the contents to be used as the heading template: '', // In effect remove this element! replace: true, - require: '^accordionGroup', - link: function(scope, element, attr, accordionGroupCtrl, transclude) { + require: '^uibAccordionGroup', + link: function(scope, element, attrs, accordionGroupCtrl, transclude) { // Pass the heading to the accordion-group controller // so that it can be transcluded into the right place in the template // [The second parameter to transclude causes the elements to be cloned so that they work in ng-repeat] - accordionGroupCtrl.setHeading(transclude(scope, function() {})); + accordionGroupCtrl.setHeading(transclude(scope, angular.noop)); } }; }) // Use in the accordion-group template to indicate where you want the heading to be transcluded // You must provide the property on the accordion-group controller that will hold the transcluded element -//
-// -// ... -//
-.directive('accordionTransclude', function() { +.directive('uibAccordionTransclude', function() { return { - require: '^accordionGroup', - link: function(scope, element, attr, controller) { - scope.$watch(function() { return controller[attr.accordionTransclude]; }, function(heading) { - if ( heading ) { - element.html(''); - element.append(heading); + require: '^uibAccordionGroup', + link: function(scope, element, attrs, controller) { + scope.$watch(function() { return controller[attrs.uibAccordionTransclude]; }, function(heading) { + if (heading) { + var elem = angular.element(element[0].querySelector('[uib-accordion-header]')); + elem.html(''); + elem.append(heading); } }); } @@ -299,17 +245,28 @@ angular.module('ui.bootstrap.accordion', ['ui.bootstrap.collapse']) angular.module('ui.bootstrap.alert', []) -.controller('AlertController', ['$scope', '$attrs', function ($scope, $attrs) { - $scope.closeable = 'close' in $attrs; +.controller('UibAlertController', ['$scope', '$attrs', '$interpolate', '$timeout', function($scope, $attrs, $interpolate, $timeout) { + $scope.closeable = !!$attrs.close; + + var dismissOnTimeout = angular.isDefined($attrs.dismissOnTimeout) ? + $interpolate($attrs.dismissOnTimeout)($scope.$parent) : null; + + if (dismissOnTimeout) { + $timeout(function() { + $scope.close(); + }, parseInt(dismissOnTimeout, 10)); + } }]) -.directive('alert', function () { +.directive('uibAlert', function() { return { - restrict:'EA', - controller:'AlertController', - templateUrl:'template/alert/alert.html', - transclude:true, - replace:true, + controller: 'UibAlertController', + controllerAs: 'alert', + templateUrl: function(element, attrs) { + return attrs.templateUrl || 'uib/template/alert/alert.html'; + }, + transclude: true, + replace: true, scope: { type: '@', close: '&' @@ -317,62 +274,69 @@ angular.module('ui.bootstrap.alert', []) }; }); -angular.module('ui.bootstrap.bindHtml', []) - - .directive('bindHtmlUnsafe', function () { - return function (scope, element, attr) { - element.addClass('ng-binding').data('$binding', attr.bindHtmlUnsafe); - scope.$watch(attr.bindHtmlUnsafe, function bindHtmlUnsafeWatchAction(value) { - element.html(value || ''); - }); - }; - }); angular.module('ui.bootstrap.buttons', []) -.constant('buttonConfig', { +.constant('uibButtonConfig', { activeClass: 'active', toggleEvent: 'click' }) -.controller('ButtonsController', ['buttonConfig', function(buttonConfig) { +.controller('UibButtonsController', ['uibButtonConfig', function(buttonConfig) { this.activeClass = buttonConfig.activeClass || 'active'; this.toggleEvent = buttonConfig.toggleEvent || 'click'; }]) -.directive('btnRadio', function () { +.directive('uibBtnRadio', ['$parse', function($parse) { return { - require: ['btnRadio', 'ngModel'], - controller: 'ButtonsController', - link: function (scope, element, attrs, ctrls) { + require: ['uibBtnRadio', 'ngModel'], + controller: 'UibButtonsController', + controllerAs: 'buttons', + link: function(scope, element, attrs, ctrls) { var buttonsCtrl = ctrls[0], ngModelCtrl = ctrls[1]; + var uncheckableExpr = $parse(attrs.uibUncheckable); + + element.find('input').css({display: 'none'}); //model -> UI - ngModelCtrl.$render = function () { - element.toggleClass(buttonsCtrl.activeClass, angular.equals(ngModelCtrl.$modelValue, scope.$eval(attrs.btnRadio))); + ngModelCtrl.$render = function() { + element.toggleClass(buttonsCtrl.activeClass, angular.equals(ngModelCtrl.$modelValue, scope.$eval(attrs.uibBtnRadio))); }; //ui->model - element.bind(buttonsCtrl.toggleEvent, function () { + element.on(buttonsCtrl.toggleEvent, function() { + if (attrs.disabled) { + return; + } + var isActive = element.hasClass(buttonsCtrl.activeClass); if (!isActive || angular.isDefined(attrs.uncheckable)) { - scope.$apply(function () { - ngModelCtrl.$setViewValue(isActive ? null : scope.$eval(attrs.btnRadio)); + scope.$apply(function() { + ngModelCtrl.$setViewValue(isActive ? null : scope.$eval(attrs.uibBtnRadio)); ngModelCtrl.$render(); }); } }); + + if (attrs.uibUncheckable) { + scope.$watch(uncheckableExpr, function(uncheckable) { + attrs.$set('uncheckable', uncheckable ? '' : undefined); + }); + } } }; -}) +}]) -.directive('btnCheckbox', function () { +.directive('uibBtnCheckbox', function() { return { - require: ['btnCheckbox', 'ngModel'], - controller: 'ButtonsController', - link: function (scope, element, attrs, ctrls) { + require: ['uibBtnCheckbox', 'ngModel'], + controller: 'UibButtonsController', + controllerAs: 'button', + link: function(scope, element, attrs, ctrls) { var buttonsCtrl = ctrls[0], ngModelCtrl = ctrls[1]; + element.find('input').css({display: 'none'}); + function getTrueValue() { return getCheckboxValue(attrs.btnCheckboxTrue, true); } @@ -381,19 +345,22 @@ angular.module('ui.bootstrap.buttons', []) return getCheckboxValue(attrs.btnCheckboxFalse, false); } - function getCheckboxValue(attributeValue, defaultValue) { - var val = scope.$eval(attributeValue); - return angular.isDefined(val) ? val : defaultValue; + function getCheckboxValue(attribute, defaultValue) { + return angular.isDefined(attribute) ? scope.$eval(attribute) : defaultValue; } //model -> UI - ngModelCtrl.$render = function () { + ngModelCtrl.$render = function() { element.toggleClass(buttonsCtrl.activeClass, angular.equals(ngModelCtrl.$modelValue, getTrueValue())); }; //ui->model - element.bind(buttonsCtrl.toggleEvent, function () { - scope.$apply(function () { + element.on(buttonsCtrl.toggleEvent, function() { + if (attrs.disabled) { + return; + } + + scope.$apply(function() { ngModelCtrl.$setViewValue(element.hasClass(buttonsCtrl.activeClass) ? getFalseValue() : getTrueValue()); ngModelCtrl.$render(); }); @@ -402,141 +369,139 @@ angular.module('ui.bootstrap.buttons', []) }; }); -/** -* @ngdoc overview -* @name ui.bootstrap.carousel -* -* @description -* AngularJS version of an image carousel. -* -*/ -angular.module('ui.bootstrap.carousel', ['ui.bootstrap.transition']) -.controller('CarouselController', ['$scope', '$timeout', '$transition', function ($scope, $timeout, $transition) { +angular.module('ui.bootstrap.carousel', []) + +.controller('UibCarouselController', ['$scope', '$element', '$interval', '$timeout', '$animate', function($scope, $element, $interval, $timeout, $animate) { var self = this, slides = self.slides = $scope.slides = [], - currentIndex = -1, - currentTimeout, isPlaying; - self.currentSlide = null; + SLIDE_DIRECTION = 'uib-slideDirection', + currentIndex = $scope.active, + currentInterval, isPlaying, bufferedTransitions = []; var destroyed = false; + + self.addSlide = function(slide, element) { + slides.push({ + slide: slide, + element: element + }); + slides.sort(function(a, b) { + return +a.slide.index - +b.slide.index; + }); + //if this is the first slide or the slide is set to active, select it + if (slide.index === $scope.active || slides.length === 1 && !angular.isNumber($scope.active)) { + if ($scope.$currentTransition) { + $scope.$currentTransition = null; + } + + currentIndex = slide.index; + $scope.active = slide.index; + setActive(currentIndex); + self.select(slides[findSlideIndex(slide)]); + if (slides.length === 1) { + $scope.play(); + } + } + }; + + self.getCurrentIndex = function() { + for (var i = 0; i < slides.length; i++) { + if (slides[i].slide.index === currentIndex) { + return i; + } + } + }; + + self.next = $scope.next = function() { + var newIndex = (self.getCurrentIndex() + 1) % slides.length; + + if (newIndex === 0 && $scope.noWrap()) { + $scope.pause(); + return; + } + + return self.select(slides[newIndex], 'next'); + }; + + self.prev = $scope.prev = function() { + var newIndex = self.getCurrentIndex() - 1 < 0 ? slides.length - 1 : self.getCurrentIndex() - 1; + + if ($scope.noWrap() && newIndex === slides.length - 1) { + $scope.pause(); + return; + } + + return self.select(slides[newIndex], 'prev'); + }; + + self.removeSlide = function(slide) { + var index = findSlideIndex(slide); + + var bufferedIndex = bufferedTransitions.indexOf(slides[index]); + if (bufferedIndex !== -1) { + bufferedTransitions.splice(bufferedIndex, 1); + } + + //get the index of the slide inside the carousel + slides.splice(index, 1); + if (slides.length > 0 && currentIndex === index) { + if (index >= slides.length) { + currentIndex = slides.length - 1; + $scope.active = currentIndex; + setActive(currentIndex); + self.select(slides[slides.length - 1]); + } else { + currentIndex = index; + $scope.active = currentIndex; + setActive(currentIndex); + self.select(slides[index]); + } + } else if (currentIndex > index) { + currentIndex--; + $scope.active = currentIndex; + } + + //clean the active value when no more slide + if (slides.length === 0) { + currentIndex = null; + $scope.active = null; + clearBufferedTransitions(); + } + }; + /* direction: "prev" or "next" */ self.select = $scope.select = function(nextSlide, direction) { - var nextIndex = slides.indexOf(nextSlide); + var nextIndex = findSlideIndex(nextSlide.slide); //Decide direction if it's not given if (direction === undefined) { - direction = nextIndex > currentIndex ? 'next' : 'prev'; + direction = nextIndex > self.getCurrentIndex() ? 'next' : 'prev'; } - if (nextSlide && nextSlide !== self.currentSlide) { - if ($scope.$currentTransition) { - $scope.$currentTransition.cancel(); - //Timeout so ng-class in template has time to fix classes for finished slide - $timeout(goNext); - } else { - goNext(); - } - } - function goNext() { - // Scope has been destroyed, stop here. - if (destroyed) { return; } - //If we have a slide to transition from and we have a transition type and we're allowed, go - if (self.currentSlide && angular.isString(direction) && !$scope.noTransition && nextSlide.$element) { - //We shouldn't do class manip in here, but it's the same weird thing bootstrap does. need to fix sometime - nextSlide.$element.addClass(direction); - var reflow = nextSlide.$element[0].offsetWidth; //force reflow - - //Set all other slides to stop doing their stuff for the new transition - angular.forEach(slides, function(slide) { - angular.extend(slide, {direction: '', entering: false, leaving: false, active: false}); - }); - angular.extend(nextSlide, {direction: direction, active: true, entering: true}); - angular.extend(self.currentSlide||{}, {direction: direction, leaving: true}); - - $scope.$currentTransition = $transition(nextSlide.$element, {}); - //We have to create new pointers inside a closure since next & current will change - (function(next,current) { - $scope.$currentTransition.then( - function(){ transitionDone(next, current); }, - function(){ transitionDone(next, current); } - ); - }(nextSlide, self.currentSlide)); - } else { - transitionDone(nextSlide, self.currentSlide); - } - self.currentSlide = nextSlide; - currentIndex = nextIndex; - //every time you change slides, reset the timer - restartTimer(); - } - function transitionDone(next, current) { - angular.extend(next, {direction: '', active: true, leaving: false, entering: false}); - angular.extend(current||{}, {direction: '', active: false, leaving: false, entering: false}); - $scope.$currentTransition = null; + //Prevent this user-triggered transition from occurring if there is already one in progress + if (nextSlide.slide.index !== currentIndex && + !$scope.$currentTransition) { + goNext(nextSlide.slide, nextIndex, direction); + } else if (nextSlide && nextSlide.slide.index !== currentIndex && $scope.$currentTransition) { + bufferedTransitions.push(slides[nextIndex]); } }; - $scope.$on('$destroy', function () { - destroyed = true; - }); /* Allow outside people to call indexOf on slides array */ - self.indexOfSlide = function(slide) { - return slides.indexOf(slide); - }; - - $scope.next = function() { - var newIndex = (currentIndex + 1) % slides.length; - - //Prevent this user-triggered transition from occurring if there is already one in progress - if (!$scope.$currentTransition) { - return self.select(slides[newIndex], 'next'); - } - }; - - $scope.prev = function() { - var newIndex = currentIndex - 1 < 0 ? slides.length - 1 : currentIndex - 1; - - //Prevent this user-triggered transition from occurring if there is already one in progress - if (!$scope.$currentTransition) { - return self.select(slides[newIndex], 'prev'); - } + $scope.indexOfSlide = function(slide) { + return +slide.slide.index; }; $scope.isActive = function(slide) { - return self.currentSlide === slide; + return $scope.active === slide.slide.index; }; - $scope.$watch('interval', restartTimer); - $scope.$on('$destroy', resetTimer); - - function restartTimer() { - resetTimer(); - var interval = +$scope.interval; - if (!isNaN(interval) && interval>=0) { - currentTimeout = $timeout(timerFn, interval); - } - } - - function resetTimer() { - if (currentTimeout) { - $timeout.cancel(currentTimeout); - currentTimeout = null; - } - } - - function timerFn() { - if (isPlaying) { - $scope.next(); - restartTimer(); - } else { - $scope.pause(); - } - } - - $scope.play = function() { - if (!isPlaying) { - isPlaying = true; - restartTimer(); - } + $scope.isPrevDisabled = function() { + return $scope.active === 0 && $scope.noWrap(); }; + + $scope.isNextDisabled = function() { + return $scope.active === slides.length - 1 && $scope.noWrap(); + }; + $scope.pause = function() { if (!$scope.noPause) { isPlaying = false; @@ -544,141 +509,175 @@ angular.module('ui.bootstrap.carousel', ['ui.bootstrap.transition']) } }; - self.addSlide = function(slide, element) { - slide.$element = element; - slides.push(slide); - //if this is the first slide or the slide is set to active, select it - if(slides.length === 1 || slide.active) { - self.select(slides[slides.length-1]); - if (slides.length == 1) { - $scope.play(); - } - } else { - slide.active = false; + $scope.play = function() { + if (!isPlaying) { + isPlaying = true; + restartTimer(); } }; - self.removeSlide = function(slide) { - //get the index of the slide inside the carousel - var index = slides.indexOf(slide); - slides.splice(index, 1); - if (slides.length > 0 && slide.active) { - if (index >= slides.length) { - self.select(slides[index-1]); - } else { + $scope.$on('$destroy', function() { + destroyed = true; + resetTimer(); + }); + + $scope.$watch('noTransition', function(noTransition) { + $animate.enabled($element, !noTransition); + }); + + $scope.$watch('interval', restartTimer); + + $scope.$watchCollection('slides', resetTransition); + + $scope.$watch('active', function(index) { + if (angular.isNumber(index) && currentIndex !== index) { + for (var i = 0; i < slides.length; i++) { + if (slides[i].slide.index === index) { + index = i; + break; + } + } + + var slide = slides[index]; + if (slide) { + setActive(index); self.select(slides[index]); + currentIndex = index; } - } else if (currentIndex > index) { - currentIndex--; } - }; + }); + function clearBufferedTransitions() { + while (bufferedTransitions.length) { + bufferedTransitions.shift(); + } + } + + function getSlideByIndex(index) { + for (var i = 0, l = slides.length; i < l; ++i) { + if (slides[i].index === index) { + return slides[i]; + } + } + } + + function setActive(index) { + for (var i = 0; i < slides.length; i++) { + slides[i].slide.active = i === index; + } + } + + function goNext(slide, index, direction) { + if (destroyed) { + return; + } + + angular.extend(slide, {direction: direction}); + angular.extend(slides[currentIndex].slide || {}, {direction: direction}); + if ($animate.enabled($element) && !$scope.$currentTransition && + slides[index].element && self.slides.length > 1) { + slides[index].element.data(SLIDE_DIRECTION, slide.direction); + var currentIdx = self.getCurrentIndex(); + + if (angular.isNumber(currentIdx) && slides[currentIdx].element) { + slides[currentIdx].element.data(SLIDE_DIRECTION, slide.direction); + } + + $scope.$currentTransition = true; + $animate.on('addClass', slides[index].element, function(element, phase) { + if (phase === 'close') { + $scope.$currentTransition = null; + $animate.off('addClass', element); + if (bufferedTransitions.length) { + var nextSlide = bufferedTransitions.pop().slide; + var nextIndex = nextSlide.index; + var nextDirection = nextIndex > self.getCurrentIndex() ? 'next' : 'prev'; + clearBufferedTransitions(); + + goNext(nextSlide, nextIndex, nextDirection); + } + } + }); + } + + $scope.active = slide.index; + currentIndex = slide.index; + setActive(index); + + //every time you change slides, reset the timer + restartTimer(); + } + + function findSlideIndex(slide) { + for (var i = 0; i < slides.length; i++) { + if (slides[i].slide === slide) { + return i; + } + } + } + + function resetTimer() { + if (currentInterval) { + $interval.cancel(currentInterval); + currentInterval = null; + } + } + + function resetTransition(slides) { + if (!slides.length) { + $scope.$currentTransition = null; + clearBufferedTransitions(); + } + } + + function restartTimer() { + resetTimer(); + var interval = +$scope.interval; + if (!isNaN(interval) && interval > 0) { + currentInterval = $interval(timerFn, interval); + } + } + + function timerFn() { + var interval = +$scope.interval; + if (isPlaying && !isNaN(interval) && interval > 0 && slides.length) { + $scope.next(); + } else { + $scope.pause(); + } + } }]) -/** - * @ngdoc directive - * @name ui.bootstrap.carousel.directive:carousel - * @restrict EA - * - * @description - * Carousel is the outer container for a set of image 'slides' to showcase. - * - * @param {number=} interval The time, in milliseconds, that it will take the carousel to go to the next slide. - * @param {boolean=} noTransition Whether to disable transitions on the carousel. - * @param {boolean=} noPause Whether to disable pausing on the carousel (by default, the carousel interval pauses on hover). - * - * @example - - - - - - - - - - - - - - - .carousel-indicators { - top: auto; - bottom: 15px; - } - - - */ -.directive('carousel', [function() { +.directive('uibCarousel', function() { return { - restrict: 'EA', transclude: true, replace: true, - controller: 'CarouselController', - require: 'carousel', - templateUrl: 'template/carousel/carousel.html', + controller: 'UibCarouselController', + controllerAs: 'carousel', + templateUrl: function(element, attrs) { + return attrs.templateUrl || 'uib/template/carousel/carousel.html'; + }, scope: { + active: '=', interval: '=', noTransition: '=', - noPause: '=' + noPause: '=', + noWrap: '&' } }; -}]) +}) -/** - * @ngdoc directive - * @name ui.bootstrap.carousel.directive:slide - * @restrict EA - * - * @description - * Creates a slide inside a {@link ui.bootstrap.carousel.directive:carousel carousel}. Must be placed as a child of a carousel element. - * - * @param {boolean=} active Model binding, whether or not this slide is currently active. - * - * @example - - -
- - - - - - - Interval, in milliseconds: -
Enter a negative number to stop the interval. -
-
- -function CarouselDemoCtrl($scope) { - $scope.myInterval = 5000; -} - - - .carousel-indicators { - top: auto; - bottom: 15px; - } - -
-*/ - -.directive('slide', function() { +.directive('uibSlide', function() { return { - require: '^carousel', - restrict: 'EA', + require: '^uibCarousel', transclude: true, replace: true, - templateUrl: 'template/carousel/slide.html', + templateUrl: function(element, attrs) { + return attrs.templateUrl || 'uib/template/carousel/slide.html'; + }, scope: { - active: '=?' + actual: '=?', + index: '=?' }, link: function (scope, element, attrs, carouselCtrl) { carouselCtrl.addSlide(scope, element); @@ -686,85 +685,349 @@ function CarouselDemoCtrl($scope) { scope.$on('$destroy', function() { carouselCtrl.removeSlide(scope); }); - - scope.$watch('active', function(active) { - if (active) { - carouselCtrl.select(scope); - } - }); } }; -}); +}) + +.animation('.item', ['$animateCss', +function($animateCss) { + var SLIDE_DIRECTION = 'uib-slideDirection'; + + function removeClass(element, className, callback) { + element.removeClass(className); + if (callback) { + callback(); + } + } + + return { + beforeAddClass: function(element, className, done) { + if (className === 'active') { + var stopped = false; + var direction = element.data(SLIDE_DIRECTION); + var directionClass = direction === 'next' ? 'left' : 'right'; + var removeClassFn = removeClass.bind(this, element, + directionClass + ' ' + direction, done); + element.addClass(direction); + + $animateCss(element, {addClass: directionClass}) + .start() + .done(removeClassFn); + + return function() { + stopped = true; + }; + } + done(); + }, + beforeRemoveClass: function (element, className, done) { + if (className === 'active') { + var stopped = false; + var direction = element.data(SLIDE_DIRECTION); + var directionClass = direction === 'next' ? 'left' : 'right'; + var removeClassFn = removeClass.bind(this, element, directionClass, done); + + $animateCss(element, {addClass: directionClass}) + .start() + .done(removeClassFn); + + return function() { + stopped = true; + }; + } + done(); + } + }; +}]); angular.module('ui.bootstrap.dateparser', []) -.service('dateParser', ['$locale', 'orderByFilter', function($locale, orderByFilter) { +.service('uibDateParser', ['$log', '$locale', 'dateFilter', 'orderByFilter', function($log, $locale, dateFilter, orderByFilter) { + // Pulled from https://github.com/mbostock/d3/blob/master/src/format/requote.js + var SPECIAL_CHARACTERS_REGEXP = /[\\\^\$\*\+\?\|\[\]\(\)\.\{\}]/g; - this.parsers = {}; + var localeId; + var formatCodeToRegex; - var formatCodeToRegex = { - 'yyyy': { - regex: '\\d{4}', - apply: function(value) { this.year = +value; } - }, - 'yy': { - regex: '\\d{2}', - apply: function(value) { this.year = +value + 2000; } - }, - 'y': { - regex: '\\d{1,4}', - apply: function(value) { this.year = +value; } - }, - 'MMMM': { - regex: $locale.DATETIME_FORMATS.MONTH.join('|'), - apply: function(value) { this.month = $locale.DATETIME_FORMATS.MONTH.indexOf(value); } - }, - 'MMM': { - regex: $locale.DATETIME_FORMATS.SHORTMONTH.join('|'), - apply: function(value) { this.month = $locale.DATETIME_FORMATS.SHORTMONTH.indexOf(value); } - }, - 'MM': { - regex: '0[1-9]|1[0-2]', - apply: function(value) { this.month = value - 1; } - }, - 'M': { - regex: '[1-9]|1[0-2]', - apply: function(value) { this.month = value - 1; } - }, - 'dd': { - regex: '[0-2][0-9]{1}|3[0-1]{1}', - apply: function(value) { this.date = +value; } - }, - 'd': { - regex: '[1-2]?[0-9]{1}|3[0-1]{1}', - apply: function(value) { this.date = +value; } - }, - 'EEEE': { - regex: $locale.DATETIME_FORMATS.DAY.join('|') - }, - 'EEE': { - regex: $locale.DATETIME_FORMATS.SHORTDAY.join('|') - } + this.init = function() { + localeId = $locale.id; + + this.parsers = {}; + this.formatters = {}; + + formatCodeToRegex = [ + { + key: 'yyyy', + regex: '\\d{4}', + apply: function(value) { this.year = +value; }, + formatter: function(date) { + var _date = new Date(); + _date.setFullYear(Math.abs(date.getFullYear())); + return dateFilter(_date, 'yyyy'); + } + }, + { + key: 'yy', + regex: '\\d{2}', + apply: function(value) { value = +value; this.year = value < 69 ? value + 2000 : value + 1900; }, + formatter: function(date) { + var _date = new Date(); + _date.setFullYear(Math.abs(date.getFullYear())); + return dateFilter(_date, 'yy'); + } + }, + { + key: 'y', + regex: '\\d{1,4}', + apply: function(value) { this.year = +value; }, + formatter: function(date) { + var _date = new Date(); + _date.setFullYear(Math.abs(date.getFullYear())); + return dateFilter(_date, 'y'); + } + }, + { + key: 'M!', + regex: '0?[1-9]|1[0-2]', + apply: function(value) { this.month = value - 1; }, + formatter: function(date) { + var value = date.getMonth(); + if (/^[0-9]$/.test(value)) { + return dateFilter(date, 'MM'); + } + + return dateFilter(date, 'M'); + } + }, + { + key: 'MMMM', + regex: $locale.DATETIME_FORMATS.MONTH.join('|'), + apply: function(value) { this.month = $locale.DATETIME_FORMATS.MONTH.indexOf(value); }, + formatter: function(date) { return dateFilter(date, 'MMMM'); } + }, + { + key: 'MMM', + regex: $locale.DATETIME_FORMATS.SHORTMONTH.join('|'), + apply: function(value) { this.month = $locale.DATETIME_FORMATS.SHORTMONTH.indexOf(value); }, + formatter: function(date) { return dateFilter(date, 'MMM'); } + }, + { + key: 'MM', + regex: '0[1-9]|1[0-2]', + apply: function(value) { this.month = value - 1; }, + formatter: function(date) { return dateFilter(date, 'MM'); } + }, + { + key: 'M', + regex: '[1-9]|1[0-2]', + apply: function(value) { this.month = value - 1; }, + formatter: function(date) { return dateFilter(date, 'M'); } + }, + { + key: 'd!', + regex: '[0-2]?[0-9]{1}|3[0-1]{1}', + apply: function(value) { this.date = +value; }, + formatter: function(date) { + var value = date.getDate(); + if (/^[1-9]$/.test(value)) { + return dateFilter(date, 'dd'); + } + + return dateFilter(date, 'd'); + } + }, + { + key: 'dd', + regex: '[0-2][0-9]{1}|3[0-1]{1}', + apply: function(value) { this.date = +value; }, + formatter: function(date) { return dateFilter(date, 'dd'); } + }, + { + key: 'd', + regex: '[1-2]?[0-9]{1}|3[0-1]{1}', + apply: function(value) { this.date = +value; }, + formatter: function(date) { return dateFilter(date, 'd'); } + }, + { + key: 'EEEE', + regex: $locale.DATETIME_FORMATS.DAY.join('|'), + formatter: function(date) { return dateFilter(date, 'EEEE'); } + }, + { + key: 'EEE', + regex: $locale.DATETIME_FORMATS.SHORTDAY.join('|'), + formatter: function(date) { return dateFilter(date, 'EEE'); } + }, + { + key: 'HH', + regex: '(?:0|1)[0-9]|2[0-3]', + apply: function(value) { this.hours = +value; }, + formatter: function(date) { return dateFilter(date, 'HH'); } + }, + { + key: 'hh', + regex: '0[0-9]|1[0-2]', + apply: function(value) { this.hours = +value; }, + formatter: function(date) { return dateFilter(date, 'hh'); } + }, + { + key: 'H', + regex: '1?[0-9]|2[0-3]', + apply: function(value) { this.hours = +value; }, + formatter: function(date) { return dateFilter(date, 'H'); } + }, + { + key: 'h', + regex: '[0-9]|1[0-2]', + apply: function(value) { this.hours = +value; }, + formatter: function(date) { return dateFilter(date, 'h'); } + }, + { + key: 'mm', + regex: '[0-5][0-9]', + apply: function(value) { this.minutes = +value; }, + formatter: function(date) { return dateFilter(date, 'mm'); } + }, + { + key: 'm', + regex: '[0-9]|[1-5][0-9]', + apply: function(value) { this.minutes = +value; }, + formatter: function(date) { return dateFilter(date, 'm'); } + }, + { + key: 'sss', + regex: '[0-9][0-9][0-9]', + apply: function(value) { this.milliseconds = +value; }, + formatter: function(date) { return dateFilter(date, 'sss'); } + }, + { + key: 'ss', + regex: '[0-5][0-9]', + apply: function(value) { this.seconds = +value; }, + formatter: function(date) { return dateFilter(date, 'ss'); } + }, + { + key: 's', + regex: '[0-9]|[1-5][0-9]', + apply: function(value) { this.seconds = +value; }, + formatter: function(date) { return dateFilter(date, 's'); } + }, + { + key: 'a', + regex: $locale.DATETIME_FORMATS.AMPMS.join('|'), + apply: function(value) { + if (this.hours === 12) { + this.hours = 0; + } + + if (value === 'PM') { + this.hours += 12; + } + }, + formatter: function(date) { return dateFilter(date, 'a'); } + }, + { + key: 'Z', + regex: '[+-]\\d{4}', + apply: function(value) { + var matches = value.match(/([+-])(\d{2})(\d{2})/), + sign = matches[1], + hours = matches[2], + minutes = matches[3]; + this.hours += toInt(sign + hours); + this.minutes += toInt(sign + minutes); + }, + formatter: function(date) { + return dateFilter(date, 'Z'); + } + }, + { + key: 'ww', + regex: '[0-4][0-9]|5[0-3]', + formatter: function(date) { return dateFilter(date, 'ww'); } + }, + { + key: 'w', + regex: '[0-9]|[1-4][0-9]|5[0-3]', + formatter: function(date) { return dateFilter(date, 'w'); } + }, + { + key: 'GGGG', + regex: $locale.DATETIME_FORMATS.ERANAMES.join('|').replace(/\s/g, '\\s'), + formatter: function(date) { return dateFilter(date, 'GGGG'); } + }, + { + key: 'GGG', + regex: $locale.DATETIME_FORMATS.ERAS.join('|'), + formatter: function(date) { return dateFilter(date, 'GGG'); } + }, + { + key: 'GG', + regex: $locale.DATETIME_FORMATS.ERAS.join('|'), + formatter: function(date) { return dateFilter(date, 'GG'); } + }, + { + key: 'G', + regex: $locale.DATETIME_FORMATS.ERAS.join('|'), + formatter: function(date) { return dateFilter(date, 'G'); } + } + ]; }; - this.createParser = function(format) { + this.init(); + + function createParser(format, func) { var map = [], regex = format.split(''); - angular.forEach(formatCodeToRegex, function(data, code) { - var index = format.indexOf(code); + // check for literal values + var quoteIndex = format.indexOf('\''); + if (quoteIndex > -1) { + var inLiteral = false; + format = format.split(''); + for (var i = quoteIndex; i < format.length; i++) { + if (inLiteral) { + if (format[i] === '\'') { + if (i + 1 < format.length && format[i+1] === '\'') { // escaped single quote + format[i+1] = '$'; + regex[i+1] = ''; + } else { // end of literal + regex[i] = ''; + inLiteral = false; + } + } + format[i] = '$'; + } else { + if (format[i] === '\'') { // start of literal + format[i] = '$'; + regex[i] = ''; + inLiteral = true; + } + } + } + + format = format.join(''); + } + + angular.forEach(formatCodeToRegex, function(data) { + var index = format.indexOf(data.key); if (index > -1) { format = format.split(''); regex[index] = '(' + data.regex + ')'; format[index] = '$'; // Custom symbol to define consumed part of format - for (var i = index + 1, n = index + code.length; i < n; i++) { + for (var i = index + 1, n = index + data.key.length; i < n; i++) { regex[i] = ''; format[i] = '$'; } format = format.join(''); - map.push({ index: index, apply: data.apply }); + map.push({ + index: index, + key: data.key, + apply: data[func], + matcher: data.regex + }); } }); @@ -772,36 +1035,113 @@ angular.module('ui.bootstrap.dateparser', []) regex: new RegExp('^' + regex.join('') + '$'), map: orderByFilter(map, 'index') }; - }; + } - this.parse = function(input, format) { - if ( !angular.isString(input) ) { - return input; + this.filter = function(date, format) { + if (!angular.isDate(date) || isNaN(date) || !format) { + return ''; } format = $locale.DATETIME_FORMATS[format] || format; - if ( !this.parsers[format] ) { - this.parsers[format] = this.createParser(format); + if ($locale.id !== localeId) { + this.init(); + } + + if (!this.formatters[format]) { + this.formatters[format] = createParser(format, 'formatter'); + } + + var parser = this.formatters[format], + map = parser.map; + + var _format = format; + + return map.reduce(function(str, mapper, i) { + var match = _format.match(new RegExp('(.*)' + mapper.key)); + if (match && angular.isString(match[1])) { + str += match[1]; + _format = _format.replace(match[1] + mapper.key, ''); + } + + var endStr = i === map.length - 1 ? _format : ''; + + if (mapper.apply) { + return str + mapper.apply.call(null, date) + endStr; + } + + return str + endStr; + }, ''); + }; + + this.parse = function(input, format, baseDate) { + if (!angular.isString(input) || !format) { + return input; + } + + format = $locale.DATETIME_FORMATS[format] || format; + format = format.replace(SPECIAL_CHARACTERS_REGEXP, '\\$&'); + + if ($locale.id !== localeId) { + this.init(); + } + + if (!this.parsers[format]) { + this.parsers[format] = createParser(format, 'apply'); } var parser = this.parsers[format], regex = parser.regex, map = parser.map, - results = input.match(regex); + results = input.match(regex), + tzOffset = false; + if (results && results.length) { + var fields, dt; + if (angular.isDate(baseDate) && !isNaN(baseDate.getTime())) { + fields = { + year: baseDate.getFullYear(), + month: baseDate.getMonth(), + date: baseDate.getDate(), + hours: baseDate.getHours(), + minutes: baseDate.getMinutes(), + seconds: baseDate.getSeconds(), + milliseconds: baseDate.getMilliseconds() + }; + } else { + if (baseDate) { + $log.warn('dateparser:', 'baseDate is not a valid date'); + } + fields = { year: 1900, month: 0, date: 1, hours: 0, minutes: 0, seconds: 0, milliseconds: 0 }; + } - if ( results && results.length ) { - var fields = { year: 1900, month: 0, date: 1, hours: 0 }, dt; + for (var i = 1, n = results.length; i < n; i++) { + var mapper = map[i - 1]; + if (mapper.matcher === 'Z') { + tzOffset = true; + } - for( var i = 1, n = results.length; i < n; i++ ) { - var mapper = map[i-1]; - if ( mapper.apply ) { + if (mapper.apply) { mapper.apply.call(fields, results[i]); } } - if ( isValid(fields.year, fields.month, fields.date) ) { - dt = new Date( fields.year, fields.month, fields.date, fields.hours); + var datesetter = tzOffset ? Date.prototype.setUTCFullYear : + Date.prototype.setFullYear; + var timesetter = tzOffset ? Date.prototype.setUTCHours : + Date.prototype.setHours; + + if (isValid(fields.year, fields.month, fields.date)) { + if (angular.isDate(baseDate) && !isNaN(baseDate.getTime()) && !tzOffset) { + dt = new Date(baseDate); + datesetter.call(dt, fields.year, fields.month, fields.date); + timesetter.call(dt, fields.hours, fields.minutes, + fields.seconds, fields.milliseconds); + } else { + dt = new Date(0); + datesetter.call(dt, fields.year, fields.month, fields.date); + timesetter.call(dt, fields.hours || 0, fields.minutes || 0, + fields.seconds || 0, fields.milliseconds || 0); + } } return dt; @@ -811,218 +1151,302 @@ angular.module('ui.bootstrap.dateparser', []) // Check if date is valid for specific month (and year for February). // Month: 0 = Jan, 1 = Feb, etc function isValid(year, month, date) { - if ( month === 1 && date > 28) { - return date === 29 && ((year % 4 === 0 && year % 100 !== 0) || year % 400 === 0); + if (date < 1) { + return false; } - if ( month === 3 || month === 5 || month === 8 || month === 10) { - return date < 31; + if (month === 1 && date > 28) { + return date === 29 && (year % 4 === 0 && year % 100 !== 0 || year % 400 === 0); + } + + if (month === 3 || month === 5 || month === 8 || month === 10) { + return date < 31; } return true; } + + function toInt(str) { + return parseInt(str, 10); + } + + this.toTimezone = toTimezone; + this.fromTimezone = fromTimezone; + this.timezoneToOffset = timezoneToOffset; + this.addDateMinutes = addDateMinutes; + this.convertTimezoneToLocal = convertTimezoneToLocal; + + function toTimezone(date, timezone) { + return date && timezone ? convertTimezoneToLocal(date, timezone) : date; + } + + function fromTimezone(date, timezone) { + return date && timezone ? convertTimezoneToLocal(date, timezone, true) : date; + } + + //https://github.com/angular/angular.js/blob/4daafd3dbe6a80d578f5a31df1bb99c77559543e/src/Angular.js#L1207 + function timezoneToOffset(timezone, fallback) { + var requestedTimezoneOffset = Date.parse('Jan 01, 1970 00:00:00 ' + timezone) / 60000; + return isNaN(requestedTimezoneOffset) ? fallback : requestedTimezoneOffset; + } + + function addDateMinutes(date, minutes) { + date = new Date(date.getTime()); + date.setMinutes(date.getMinutes() + minutes); + return date; + } + + function convertTimezoneToLocal(date, timezone, reverse) { + reverse = reverse ? -1 : 1; + var timezoneOffset = timezoneToOffset(timezone, date.getTimezoneOffset()); + return addDateMinutes(date, reverse * (timezoneOffset - date.getTimezoneOffset())); + } }]); -angular.module('ui.bootstrap.position', []) +// Avoiding use of ng-class as it creates a lot of watchers when a class is to be applied to +// at most one element. +angular.module('ui.bootstrap.isClass', []) +.directive('uibIsClass', [ + '$animate', +function ($animate) { + // 11111111 22222222 + var ON_REGEXP = /^\s*([\s\S]+?)\s+on\s+([\s\S]+?)\s*$/; + // 11111111 22222222 + var IS_REGEXP = /^\s*([\s\S]+?)\s+for\s+([\s\S]+?)\s*$/; -/** - * A set of utility methods that can be use to retrieve position of DOM elements. - * It is meant to be used where we need to absolute-position DOM elements in - * relation to other, existing elements (this is the case for tooltips, popovers, - * typeahead suggestions etc.). - */ - .factory('$position', ['$document', '$window', function ($document, $window) { + var dataPerTracked = {}; - function getStyle(el, cssprop) { - if (el.currentStyle) { //IE - return el.currentStyle[cssprop]; - } else if ($window.getComputedStyle) { - return $window.getComputedStyle(el)[cssprop]; + return { + restrict: 'A', + compile: function(tElement, tAttrs) { + var linkedScopes = []; + var instances = []; + var expToData = {}; + var lastActivated = null; + var onExpMatches = tAttrs.uibIsClass.match(ON_REGEXP); + var onExp = onExpMatches[2]; + var expsStr = onExpMatches[1]; + var exps = expsStr.split(','); + + return linkFn; + + function linkFn(scope, element, attrs) { + linkedScopes.push(scope); + instances.push({ + scope: scope, + element: element + }); + + exps.forEach(function(exp, k) { + addForExp(exp, scope); + }); + + scope.$on('$destroy', removeScope); } - // finally try and get inline style - return el.style[cssprop]; - } - /** - * Checks if a given element is statically positioned - * @param element - raw DOM element - */ - function isStaticPositioned(element) { - return (getStyle(element, 'position') || 'static' ) === 'static'; - } - - /** - * returns the closest, non-statically positioned parentOffset of a given element - * @param element - */ - var parentOffsetEl = function (element) { - var docDomEl = $document[0]; - var offsetParent = element.offsetParent || docDomEl; - while (offsetParent && offsetParent !== docDomEl && isStaticPositioned(offsetParent) ) { - offsetParent = offsetParent.offsetParent; - } - return offsetParent || docDomEl; - }; - - return { - /** - * Provides read-only equivalent of jQuery's position function: - * http://api.jquery.com/position/ - */ - position: function (element) { - var elBCR = this.offset(element); - var offsetParentBCR = { top: 0, left: 0 }; - var offsetParentEl = parentOffsetEl(element[0]); - if (offsetParentEl != $document[0]) { - offsetParentBCR = this.offset(angular.element(offsetParentEl)); - offsetParentBCR.top += offsetParentEl.clientTop - offsetParentEl.scrollTop; - offsetParentBCR.left += offsetParentEl.clientLeft - offsetParentEl.scrollLeft; + function addForExp(exp, scope) { + var matches = exp.match(IS_REGEXP); + var clazz = scope.$eval(matches[1]); + var compareWithExp = matches[2]; + var data = expToData[exp]; + if (!data) { + var watchFn = function(compareWithVal) { + var newActivated = null; + instances.some(function(instance) { + var thisVal = instance.scope.$eval(onExp); + if (thisVal === compareWithVal) { + newActivated = instance; + return true; + } + }); + if (data.lastActivated !== newActivated) { + if (data.lastActivated) { + $animate.removeClass(data.lastActivated.element, clazz); + } + if (newActivated) { + $animate.addClass(newActivated.element, clazz); + } + data.lastActivated = newActivated; + } + }; + expToData[exp] = data = { + lastActivated: null, + scope: scope, + watchFn: watchFn, + compareWithExp: compareWithExp, + watcher: scope.$watch(compareWithExp, watchFn) + }; } - - var boundingClientRect = element[0].getBoundingClientRect(); - return { - width: boundingClientRect.width || element.prop('offsetWidth'), - height: boundingClientRect.height || element.prop('offsetHeight'), - top: elBCR.top - offsetParentBCR.top, - left: elBCR.left - offsetParentBCR.left - }; - }, - - /** - * Provides read-only equivalent of jQuery's offset function: - * http://api.jquery.com/offset/ - */ - offset: function (element) { - var boundingClientRect = element[0].getBoundingClientRect(); - return { - width: boundingClientRect.width || element.prop('offsetWidth'), - height: boundingClientRect.height || element.prop('offsetHeight'), - top: boundingClientRect.top + ($window.pageYOffset || $document[0].documentElement.scrollTop), - left: boundingClientRect.left + ($window.pageXOffset || $document[0].documentElement.scrollLeft) - }; - }, - - /** - * Provides coordinates for the targetEl in relation to hostEl - */ - positionElements: function (hostEl, targetEl, positionStr, appendToBody) { - - var positionStrParts = positionStr.split('-'); - var pos0 = positionStrParts[0], pos1 = positionStrParts[1] || 'center'; - - var hostElPos, - targetElWidth, - targetElHeight, - targetElPos; - - hostElPos = appendToBody ? this.offset(hostEl) : this.position(hostEl); - - targetElWidth = targetEl.prop('offsetWidth'); - targetElHeight = targetEl.prop('offsetHeight'); - - var shiftWidth = { - center: function () { - return hostElPos.left + hostElPos.width / 2 - targetElWidth / 2; - }, - left: function () { - return hostElPos.left; - }, - right: function () { - return hostElPos.left + hostElPos.width; - } - }; - - var shiftHeight = { - center: function () { - return hostElPos.top + hostElPos.height / 2 - targetElHeight / 2; - }, - top: function () { - return hostElPos.top; - }, - bottom: function () { - return hostElPos.top + hostElPos.height; - } - }; - - switch (pos0) { - case 'right': - targetElPos = { - top: shiftHeight[pos1](), - left: shiftWidth[pos0]() - }; - break; - case 'left': - targetElPos = { - top: shiftHeight[pos1](), - left: hostElPos.left - targetElWidth - }; - break; - case 'bottom': - targetElPos = { - top: shiftHeight[pos0](), - left: shiftWidth[pos1]() - }; - break; - default: - targetElPos = { - top: hostElPos.top - targetElHeight, - left: shiftWidth[pos1]() - }; - break; - } - - return targetElPos; + data.watchFn(scope.$eval(compareWithExp)); } - }; - }]); -angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootstrap.position']) + function removeScope(e) { + var removedScope = e.targetScope; + var index = linkedScopes.indexOf(removedScope); + linkedScopes.splice(index, 1); + instances.splice(index, 1); + if (linkedScopes.length) { + var newWatchScope = linkedScopes[0]; + angular.forEach(expToData, function(data) { + if (data.scope === removedScope) { + data.watcher = newWatchScope.$watch(data.compareWithExp, data.watchFn); + data.scope = newWatchScope; + } + }); + } else { + expToData = {}; + } + } + } + }; +}]); +angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootstrap.isClass']) -.constant('datepickerConfig', { +.value('$datepickerSuppressError', false) + +.value('$datepickerLiteralWarning', true) + +.constant('uibDatepickerConfig', { + datepickerMode: 'day', formatDay: 'dd', formatMonth: 'MMMM', formatYear: 'yyyy', formatDayHeader: 'EEE', formatDayTitle: 'MMMM yyyy', formatMonthTitle: 'yyyy', - datepickerMode: 'day', - minMode: 'day', + maxDate: null, maxMode: 'year', - showWeeks: true, - startingDay: 0, - yearRange: 20, minDate: null, - maxDate: null + minMode: 'day', + ngModelOptions: {}, + shortcutPropagation: false, + showWeeks: true, + yearColumns: 5, + yearRows: 4 }) -.controller('DatepickerController', ['$scope', '$attrs', '$parse', '$interpolate', '$timeout', '$log', 'dateFilter', 'datepickerConfig', function($scope, $attrs, $parse, $interpolate, $timeout, $log, dateFilter, datepickerConfig) { +.controller('UibDatepickerController', ['$scope', '$attrs', '$parse', '$interpolate', '$locale', '$log', 'dateFilter', 'uibDatepickerConfig', '$datepickerLiteralWarning', '$datepickerSuppressError', 'uibDateParser', + function($scope, $attrs, $parse, $interpolate, $locale, $log, dateFilter, datepickerConfig, $datepickerLiteralWarning, $datepickerSuppressError, dateParser) { var self = this, - ngModelCtrl = { $setViewValue: angular.noop }; // nullModelCtrl; + ngModelCtrl = { $setViewValue: angular.noop }, // nullModelCtrl; + ngModelOptions = {}, + watchListeners = [], + optionsUsed = !!$attrs.datepickerOptions; + + if (!$scope.datepickerOptions) { + $scope.datepickerOptions = {}; + } // Modes chain this.modes = ['day', 'month', 'year']; - // Configuration attributes - angular.forEach(['formatDay', 'formatMonth', 'formatYear', 'formatDayHeader', 'formatDayTitle', 'formatMonthTitle', - 'minMode', 'maxMode', 'showWeeks', 'startingDay', 'yearRange'], function( key, index ) { - self[key] = angular.isDefined($attrs[key]) ? (index < 8 ? $interpolate($attrs[key])($scope.$parent) : $scope.$parent.$eval($attrs[key])) : datepickerConfig[key]; - }); + [ + 'customClass', + 'dateDisabled', + 'datepickerMode', + 'formatDay', + 'formatDayHeader', + 'formatDayTitle', + 'formatMonth', + 'formatMonthTitle', + 'formatYear', + 'maxDate', + 'maxMode', + 'minDate', + 'minMode', + 'showWeeks', + 'shortcutPropagation', + 'startingDay', + 'yearColumns', + 'yearRows' + ].forEach(function(key) { + switch (key) { + case 'customClass': + case 'dateDisabled': + $scope[key] = $scope.datepickerOptions[key] || angular.noop; + break; + case 'datepickerMode': + $scope.datepickerMode = angular.isDefined($scope.datepickerOptions.datepickerMode) ? + $scope.datepickerOptions.datepickerMode : datepickerConfig.datepickerMode; + break; + case 'formatDay': + case 'formatDayHeader': + case 'formatDayTitle': + case 'formatMonth': + case 'formatMonthTitle': + case 'formatYear': + self[key] = angular.isDefined($scope.datepickerOptions[key]) ? + $interpolate($scope.datepickerOptions[key])($scope.$parent) : + datepickerConfig[key]; + break; + case 'showWeeks': + case 'shortcutPropagation': + case 'yearColumns': + case 'yearRows': + self[key] = angular.isDefined($scope.datepickerOptions[key]) ? + $scope.datepickerOptions[key] : datepickerConfig[key]; + break; + case 'startingDay': + if (angular.isDefined($scope.datepickerOptions.startingDay)) { + self.startingDay = $scope.datepickerOptions.startingDay; + } else if (angular.isNumber(datepickerConfig.startingDay)) { + self.startingDay = datepickerConfig.startingDay; + } else { + self.startingDay = ($locale.DATETIME_FORMATS.FIRSTDAYOFWEEK + 8) % 7; + } - // Watchable attributes - angular.forEach(['minDate', 'maxDate'], function( key ) { - if ( $attrs[key] ) { - $scope.$parent.$watch($parse($attrs[key]), function(value) { - self[key] = value ? new Date(value) : null; - self.refreshView(); - }); - } else { - self[key] = datepickerConfig[key] ? new Date(datepickerConfig[key]) : null; + break; + case 'maxDate': + case 'minDate': + $scope.$watch('datepickerOptions.' + key, function(value) { + if (value) { + if (angular.isDate(value)) { + self[key] = dateParser.fromTimezone(new Date(value), ngModelOptions.timezone); + } else { + if ($datepickerLiteralWarning) { + $log.warn('Literal date support has been deprecated, please switch to date object usage'); + } + + self[key] = new Date(dateFilter(value, 'medium')); + } + } else { + self[key] = datepickerConfig[key] ? + dateParser.fromTimezone(new Date(datepickerConfig[key]), ngModelOptions.timezone) : + null; + } + + self.refreshView(); + }); + + break; + case 'maxMode': + case 'minMode': + if ($scope.datepickerOptions[key]) { + $scope.$watch(function() { return $scope.datepickerOptions[key]; }, function(value) { + self[key] = $scope[key] = angular.isDefined(value) ? value : datepickerOptions[key]; + if (key === 'minMode' && self.modes.indexOf($scope.datepickerOptions.datepickerMode) < self.modes.indexOf(self[key]) || + key === 'maxMode' && self.modes.indexOf($scope.datepickerOptions.datepickerMode) > self.modes.indexOf(self[key])) { + $scope.datepickerMode = self[key]; + $scope.datepickerOptions.datepickerMode = self[key]; + } + }); + } else { + self[key] = $scope[key] = datepickerConfig[key] || null; + } + + break; } }); - $scope.datepickerMode = $scope.datepickerMode || datepickerConfig.datepickerMode; $scope.uniqueId = 'datepicker-' + $scope.$id + '-' + Math.floor(Math.random() * 10000); - this.activeDate = angular.isDefined($attrs.initDate) ? $scope.$parent.$eval($attrs.initDate) : new Date(); + + $scope.disabled = angular.isDefined($attrs.disabled) || false; + if (angular.isDefined($attrs.ngDisabled)) { + watchListeners.push($scope.$parent.$watch($attrs.ngDisabled, function(disabled) { + $scope.disabled = disabled; + self.refreshView(); + })); + } $scope.isActive = function(dateObject) { if (self.compare(dateObject.date, self.activeDate) === 0) { @@ -1032,8 +1456,24 @@ angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootst return false; }; - this.init = function( ngModelCtrl_ ) { + this.init = function(ngModelCtrl_) { ngModelCtrl = ngModelCtrl_; + ngModelOptions = ngModelCtrl_.$options || datepickerConfig.ngModelOptions; + if ($scope.datepickerOptions.initDate) { + self.activeDate = dateParser.fromTimezone($scope.datepickerOptions.initDate, ngModelOptions.timezone) || new Date(); + $scope.$watch('datepickerOptions.initDate', function(initDate) { + if (initDate && (ngModelCtrl.$isEmpty(ngModelCtrl.$modelValue) || ngModelCtrl.$invalid)) { + self.activeDate = dateParser.fromTimezone(initDate, ngModelOptions.timezone); + self.refreshView(); + } + }); + } else { + self.activeDate = new Date(); + } + + this.activeDate = ngModelCtrl.$modelValue ? + dateParser.fromTimezone(new Date(ngModelCtrl.$modelValue), ngModelOptions.timezone) : + dateParser.fromTimezone(new Date(), ngModelOptions.timezone); ngModelCtrl.$render = function() { self.render(); @@ -1041,42 +1481,71 @@ angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootst }; this.render = function() { - if ( ngModelCtrl.$modelValue ) { - var date = new Date( ngModelCtrl.$modelValue ), + if (ngModelCtrl.$viewValue) { + var date = new Date(ngModelCtrl.$viewValue), isValid = !isNaN(date); - if ( isValid ) { - this.activeDate = date; - } else { - $log.error('Datepicker directive: "ng-model" value must be a Date object, a number of milliseconds since 01.01.1970 or a string representing an RFC2822 or ISO 8601 date.'); + if (isValid) { + this.activeDate = dateParser.fromTimezone(date, ngModelOptions.timezone); + } else if (!$datepickerSuppressError) { + $log.error('Datepicker directive: "ng-model" value must be a Date object'); } - ngModelCtrl.$setValidity('date', isValid); } this.refreshView(); }; this.refreshView = function() { - if ( this.element ) { + if (this.element) { + $scope.selectedDt = null; this._refreshView(); + if ($scope.activeDt) { + $scope.activeDateId = $scope.activeDt.uid; + } - var date = ngModelCtrl.$modelValue ? new Date(ngModelCtrl.$modelValue) : null; - ngModelCtrl.$setValidity('date-disabled', !date || (this.element && !this.isDisabled(date))); + var date = ngModelCtrl.$viewValue ? new Date(ngModelCtrl.$viewValue) : null; + date = dateParser.fromTimezone(date, ngModelOptions.timezone); + ngModelCtrl.$setValidity('dateDisabled', !date || + this.element && !this.isDisabled(date)); } }; this.createDateObject = function(date, format) { - var model = ngModelCtrl.$modelValue ? new Date(ngModelCtrl.$modelValue) : null; - return { + var model = ngModelCtrl.$viewValue ? new Date(ngModelCtrl.$viewValue) : null; + model = dateParser.fromTimezone(model, ngModelOptions.timezone); + var today = new Date(); + today = dateParser.fromTimezone(today, ngModelOptions.timezone); + var time = this.compare(date, today); + var dt = { date: date, - label: dateFilter(date, format), + label: dateParser.filter(date, format), selected: model && this.compare(date, model) === 0, disabled: this.isDisabled(date), - current: this.compare(date, new Date()) === 0 + past: time < 0, + current: time === 0, + future: time > 0, + customClass: this.customClass(date) || null }; + + if (model && this.compare(date, model) === 0) { + $scope.selectedDt = dt; + } + + if (self.activeDate && this.compare(dt.date, self.activeDate) === 0) { + $scope.activeDt = dt; + } + + return dt; }; - this.isDisabled = function( date ) { - return ((this.minDate && this.compare(date, this.minDate) < 0) || (this.maxDate && this.compare(date, this.maxDate) > 0) || ($attrs.dateDisabled && $scope.dateDisabled({date: date, mode: $scope.datepickerMode}))); + this.isDisabled = function(date) { + return $scope.disabled || + this.minDate && this.compare(date, this.minDate) < 0 || + this.maxDate && this.compare(date, this.maxDate) > 0 || + $scope.dateDisabled && $scope.dateDisabled({date: date, mode: $scope.datepickerMode}); + }; + + this.customClass = function(date) { + return $scope.customClass({date: date, mode: $scope.datepickerMode}); }; // Split array into smaller arrays @@ -1088,579 +1557,1563 @@ angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootst return arrays; }; - $scope.select = function( date ) { - if ( $scope.datepickerMode === self.minMode ) { - var dt = ngModelCtrl.$modelValue ? new Date( ngModelCtrl.$modelValue ) : new Date(0, 0, 0, 0, 0, 0, 0); - dt.setFullYear( date.getFullYear(), date.getMonth(), date.getDate() ); - ngModelCtrl.$setViewValue( dt ); + $scope.select = function(date) { + if ($scope.datepickerMode === self.minMode) { + var dt = ngModelCtrl.$viewValue ? dateParser.fromTimezone(new Date(ngModelCtrl.$viewValue), ngModelOptions.timezone) : new Date(0, 0, 0, 0, 0, 0, 0); + dt.setFullYear(date.getFullYear(), date.getMonth(), date.getDate()); + dt = dateParser.toTimezone(dt, ngModelOptions.timezone); + ngModelCtrl.$setViewValue(dt); ngModelCtrl.$render(); } else { self.activeDate = date; - $scope.datepickerMode = self.modes[ self.modes.indexOf( $scope.datepickerMode ) - 1 ]; + setMode(self.modes[self.modes.indexOf($scope.datepickerMode) - 1]); + + $scope.$emit('uib:datepicker.mode'); } + + $scope.$broadcast('uib:datepicker.focus'); }; - $scope.move = function( direction ) { + $scope.move = function(direction) { var year = self.activeDate.getFullYear() + direction * (self.step.years || 0), month = self.activeDate.getMonth() + direction * (self.step.months || 0); self.activeDate.setFullYear(year, month, 1); self.refreshView(); }; - $scope.toggleMode = function( direction ) { + $scope.toggleMode = function(direction) { direction = direction || 1; - if (($scope.datepickerMode === self.maxMode && direction === 1) || ($scope.datepickerMode === self.minMode && direction === -1)) { + if ($scope.datepickerMode === self.maxMode && direction === 1 || + $scope.datepickerMode === self.minMode && direction === -1) { return; } - $scope.datepickerMode = self.modes[ self.modes.indexOf( $scope.datepickerMode ) + direction ]; + setMode(self.modes[self.modes.indexOf($scope.datepickerMode) + direction]); + + $scope.$emit('uib:datepicker.mode'); }; // Key event mapper - $scope.keys = { 13:'enter', 32:'space', 33:'pageup', 34:'pagedown', 35:'end', 36:'home', 37:'left', 38:'up', 39:'right', 40:'down' }; + $scope.keys = { 13: 'enter', 32: 'space', 33: 'pageup', 34: 'pagedown', 35: 'end', 36: 'home', 37: 'left', 38: 'up', 39: 'right', 40: 'down' }; var focusElement = function() { - $timeout(function() { - self.element[0].focus(); - }, 0 , false); + self.element[0].focus(); }; // Listen for focus requests from popup directive - $scope.$on('datepicker.focus', focusElement); + $scope.$on('uib:datepicker.focus', focusElement); - $scope.keydown = function( evt ) { + $scope.keydown = function(evt) { var key = $scope.keys[evt.which]; - if ( !key || evt.shiftKey || evt.altKey ) { + if (!key || evt.shiftKey || evt.altKey || $scope.disabled) { return; } evt.preventDefault(); - evt.stopPropagation(); + if (!self.shortcutPropagation) { + evt.stopPropagation(); + } if (key === 'enter' || key === 'space') { - if ( self.isDisabled(self.activeDate)) { + if (self.isDisabled(self.activeDate)) { return; // do nothing } $scope.select(self.activeDate); - focusElement(); } else if (evt.ctrlKey && (key === 'up' || key === 'down')) { $scope.toggleMode(key === 'up' ? 1 : -1); - focusElement(); } else { self.handleKeyDown(key, evt); self.refreshView(); } }; + + $scope.$on('$destroy', function() { + //Clear all watch listeners on destroy + while (watchListeners.length) { + watchListeners.shift()(); + } + }); + + function setMode(mode) { + $scope.datepickerMode = mode; + $scope.datepickerOptions.datepickerMode = mode; + } }]) -.directive( 'datepicker', function () { +.controller('UibDaypickerController', ['$scope', '$element', 'dateFilter', function(scope, $element, dateFilter) { + var DAYS_IN_MONTH = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; + + this.step = { months: 1 }; + this.element = $element; + function getDaysInMonth(year, month) { + return month === 1 && year % 4 === 0 && + (year % 100 !== 0 || year % 400 === 0) ? 29 : DAYS_IN_MONTH[month]; + } + + this.init = function(ctrl) { + angular.extend(ctrl, this); + scope.showWeeks = ctrl.showWeeks; + ctrl.refreshView(); + }; + + this.getDates = function(startDate, n) { + var dates = new Array(n), current = new Date(startDate), i = 0, date; + while (i < n) { + date = new Date(current); + dates[i++] = date; + current.setDate(current.getDate() + 1); + } + return dates; + }; + + this._refreshView = function() { + var year = this.activeDate.getFullYear(), + month = this.activeDate.getMonth(), + firstDayOfMonth = new Date(this.activeDate); + + firstDayOfMonth.setFullYear(year, month, 1); + + var difference = this.startingDay - firstDayOfMonth.getDay(), + numDisplayedFromPreviousMonth = difference > 0 ? + 7 - difference : - difference, + firstDate = new Date(firstDayOfMonth); + + if (numDisplayedFromPreviousMonth > 0) { + firstDate.setDate(-numDisplayedFromPreviousMonth + 1); + } + + // 42 is the number of days on a six-week calendar + var days = this.getDates(firstDate, 42); + for (var i = 0; i < 42; i ++) { + days[i] = angular.extend(this.createDateObject(days[i], this.formatDay), { + secondary: days[i].getMonth() !== month, + uid: scope.uniqueId + '-' + i + }); + } + + scope.labels = new Array(7); + for (var j = 0; j < 7; j++) { + scope.labels[j] = { + abbr: dateFilter(days[j].date, this.formatDayHeader), + full: dateFilter(days[j].date, 'EEEE') + }; + } + + scope.title = dateFilter(this.activeDate, this.formatDayTitle); + scope.rows = this.split(days, 7); + + if (scope.showWeeks) { + scope.weekNumbers = []; + var thursdayIndex = (4 + 7 - this.startingDay) % 7, + numWeeks = scope.rows.length; + for (var curWeek = 0; curWeek < numWeeks; curWeek++) { + scope.weekNumbers.push( + getISO8601WeekNumber(scope.rows[curWeek][thursdayIndex].date)); + } + } + }; + + this.compare = function(date1, date2) { + var _date1 = new Date(date1.getFullYear(), date1.getMonth(), date1.getDate()); + var _date2 = new Date(date2.getFullYear(), date2.getMonth(), date2.getDate()); + _date1.setFullYear(date1.getFullYear()); + _date2.setFullYear(date2.getFullYear()); + return _date1 - _date2; + }; + + function getISO8601WeekNumber(date) { + var checkDate = new Date(date); + checkDate.setDate(checkDate.getDate() + 4 - (checkDate.getDay() || 7)); // Thursday + var time = checkDate.getTime(); + checkDate.setMonth(0); // Compare with Jan 1 + checkDate.setDate(1); + return Math.floor(Math.round((time - checkDate) / 86400000) / 7) + 1; + } + + this.handleKeyDown = function(key, evt) { + var date = this.activeDate.getDate(); + + if (key === 'left') { + date = date - 1; + } else if (key === 'up') { + date = date - 7; + } else if (key === 'right') { + date = date + 1; + } else if (key === 'down') { + date = date + 7; + } else if (key === 'pageup' || key === 'pagedown') { + var month = this.activeDate.getMonth() + (key === 'pageup' ? - 1 : 1); + this.activeDate.setMonth(month, 1); + date = Math.min(getDaysInMonth(this.activeDate.getFullYear(), this.activeDate.getMonth()), date); + } else if (key === 'home') { + date = 1; + } else if (key === 'end') { + date = getDaysInMonth(this.activeDate.getFullYear(), this.activeDate.getMonth()); + } + this.activeDate.setDate(date); + }; +}]) + +.controller('UibMonthpickerController', ['$scope', '$element', 'dateFilter', function(scope, $element, dateFilter) { + this.step = { years: 1 }; + this.element = $element; + + this.init = function(ctrl) { + angular.extend(ctrl, this); + ctrl.refreshView(); + }; + + this._refreshView = function() { + var months = new Array(12), + year = this.activeDate.getFullYear(), + date; + + for (var i = 0; i < 12; i++) { + date = new Date(this.activeDate); + date.setFullYear(year, i, 1); + months[i] = angular.extend(this.createDateObject(date, this.formatMonth), { + uid: scope.uniqueId + '-' + i + }); + } + + scope.title = dateFilter(this.activeDate, this.formatMonthTitle); + scope.rows = this.split(months, 3); + }; + + this.compare = function(date1, date2) { + var _date1 = new Date(date1.getFullYear(), date1.getMonth()); + var _date2 = new Date(date2.getFullYear(), date2.getMonth()); + _date1.setFullYear(date1.getFullYear()); + _date2.setFullYear(date2.getFullYear()); + return _date1 - _date2; + }; + + this.handleKeyDown = function(key, evt) { + var date = this.activeDate.getMonth(); + + if (key === 'left') { + date = date - 1; + } else if (key === 'up') { + date = date - 3; + } else if (key === 'right') { + date = date + 1; + } else if (key === 'down') { + date = date + 3; + } else if (key === 'pageup' || key === 'pagedown') { + var year = this.activeDate.getFullYear() + (key === 'pageup' ? - 1 : 1); + this.activeDate.setFullYear(year); + } else if (key === 'home') { + date = 0; + } else if (key === 'end') { + date = 11; + } + this.activeDate.setMonth(date); + }; +}]) + +.controller('UibYearpickerController', ['$scope', '$element', 'dateFilter', function(scope, $element, dateFilter) { + var columns, range; + this.element = $element; + + function getStartingYear(year) { + return parseInt((year - 1) / range, 10) * range + 1; + } + + this.yearpickerInit = function() { + columns = this.yearColumns; + range = this.yearRows * columns; + this.step = { years: range }; + }; + + this._refreshView = function() { + var years = new Array(range), date; + + for (var i = 0, start = getStartingYear(this.activeDate.getFullYear()); i < range; i++) { + date = new Date(this.activeDate); + date.setFullYear(start + i, 0, 1); + years[i] = angular.extend(this.createDateObject(date, this.formatYear), { + uid: scope.uniqueId + '-' + i + }); + } + + scope.title = [years[0].label, years[range - 1].label].join(' - '); + scope.rows = this.split(years, columns); + scope.columns = columns; + }; + + this.compare = function(date1, date2) { + return date1.getFullYear() - date2.getFullYear(); + }; + + this.handleKeyDown = function(key, evt) { + var date = this.activeDate.getFullYear(); + + if (key === 'left') { + date = date - 1; + } else if (key === 'up') { + date = date - columns; + } else if (key === 'right') { + date = date + 1; + } else if (key === 'down') { + date = date + columns; + } else if (key === 'pageup' || key === 'pagedown') { + date += (key === 'pageup' ? - 1 : 1) * range; + } else if (key === 'home') { + date = getStartingYear(this.activeDate.getFullYear()); + } else if (key === 'end') { + date = getStartingYear(this.activeDate.getFullYear()) + range - 1; + } + this.activeDate.setFullYear(date); + }; +}]) + +.directive('uibDatepicker', function() { return { - restrict: 'EA', replace: true, - templateUrl: 'template/datepicker/datepicker.html', - scope: { - datepickerMode: '=?', - dateDisabled: '&' + templateUrl: function(element, attrs) { + return attrs.templateUrl || 'uib/template/datepicker/datepicker.html'; }, - require: ['datepicker', '?^ngModel'], - controller: 'DatepickerController', + scope: { + datepickerOptions: '=?' + }, + require: ['uibDatepicker', '^ngModel'], + controller: 'UibDatepickerController', + controllerAs: 'datepicker', link: function(scope, element, attrs, ctrls) { var datepickerCtrl = ctrls[0], ngModelCtrl = ctrls[1]; - if ( ngModelCtrl ) { - datepickerCtrl.init( ngModelCtrl ); - } + datepickerCtrl.init(ngModelCtrl); } }; }) -.directive('daypicker', ['dateFilter', function (dateFilter) { +.directive('uibDaypicker', function() { return { - restrict: 'EA', replace: true, - templateUrl: 'template/datepicker/day.html', - require: '^datepicker', - link: function(scope, element, attrs, ctrl) { - scope.showWeeks = ctrl.showWeeks; - - ctrl.step = { months: 1 }; - ctrl.element = element; - - var DAYS_IN_MONTH = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; - function getDaysInMonth( year, month ) { - return ((month === 1) && (year % 4 === 0) && ((year % 100 !== 0) || (year % 400 === 0))) ? 29 : DAYS_IN_MONTH[month]; - } - - function getDates(startDate, n) { - var dates = new Array(n), current = new Date(startDate), i = 0; - current.setHours(12); // Prevent repeated dates because of timezone bug - while ( i < n ) { - dates[i++] = new Date(current); - current.setDate( current.getDate() + 1 ); - } - return dates; - } - - ctrl._refreshView = function() { - var year = ctrl.activeDate.getFullYear(), - month = ctrl.activeDate.getMonth(), - firstDayOfMonth = new Date(year, month, 1), - difference = ctrl.startingDay - firstDayOfMonth.getDay(), - numDisplayedFromPreviousMonth = (difference > 0) ? 7 - difference : - difference, - firstDate = new Date(firstDayOfMonth); - - if ( numDisplayedFromPreviousMonth > 0 ) { - firstDate.setDate( - numDisplayedFromPreviousMonth + 1 ); - } - - // 42 is the number of days on a six-month calendar - var days = getDates(firstDate, 42); - for (var i = 0; i < 42; i ++) { - days[i] = angular.extend(ctrl.createDateObject(days[i], ctrl.formatDay), { - secondary: days[i].getMonth() !== month, - uid: scope.uniqueId + '-' + i - }); - } - - scope.labels = new Array(7); - for (var j = 0; j < 7; j++) { - scope.labels[j] = { - abbr: dateFilter(days[j].date, ctrl.formatDayHeader), - full: dateFilter(days[j].date, 'EEEE') - }; - } - - scope.title = dateFilter(ctrl.activeDate, ctrl.formatDayTitle); - scope.rows = ctrl.split(days, 7); - - if ( scope.showWeeks ) { - scope.weekNumbers = []; - var weekNumber = getISO8601WeekNumber( scope.rows[0][0].date ), - numWeeks = scope.rows.length; - while( scope.weekNumbers.push(weekNumber++) < numWeeks ) {} - } - }; - - ctrl.compare = function(date1, date2) { - return (new Date( date1.getFullYear(), date1.getMonth(), date1.getDate() ) - new Date( date2.getFullYear(), date2.getMonth(), date2.getDate() ) ); - }; - - function getISO8601WeekNumber(date) { - var checkDate = new Date(date); - checkDate.setDate(checkDate.getDate() + 4 - (checkDate.getDay() || 7)); // Thursday - var time = checkDate.getTime(); - checkDate.setMonth(0); // Compare with Jan 1 - checkDate.setDate(1); - return Math.floor(Math.round((time - checkDate) / 86400000) / 7) + 1; - } - - ctrl.handleKeyDown = function( key, evt ) { - var date = ctrl.activeDate.getDate(); - - if (key === 'left') { - date = date - 1; // up - } else if (key === 'up') { - date = date - 7; // down - } else if (key === 'right') { - date = date + 1; // down - } else if (key === 'down') { - date = date + 7; - } else if (key === 'pageup' || key === 'pagedown') { - var month = ctrl.activeDate.getMonth() + (key === 'pageup' ? - 1 : 1); - ctrl.activeDate.setMonth(month, 1); - date = Math.min(getDaysInMonth(ctrl.activeDate.getFullYear(), ctrl.activeDate.getMonth()), date); - } else if (key === 'home') { - date = 1; - } else if (key === 'end') { - date = getDaysInMonth(ctrl.activeDate.getFullYear(), ctrl.activeDate.getMonth()); - } - ctrl.activeDate.setDate(date); - }; - - ctrl.refreshView(); - } - }; -}]) - -.directive('monthpicker', ['dateFilter', function (dateFilter) { - return { - restrict: 'EA', - replace: true, - templateUrl: 'template/datepicker/month.html', - require: '^datepicker', - link: function(scope, element, attrs, ctrl) { - ctrl.step = { years: 1 }; - ctrl.element = element; - - ctrl._refreshView = function() { - var months = new Array(12), - year = ctrl.activeDate.getFullYear(); - - for ( var i = 0; i < 12; i++ ) { - months[i] = angular.extend(ctrl.createDateObject(new Date(year, i, 1), ctrl.formatMonth), { - uid: scope.uniqueId + '-' + i - }); - } - - scope.title = dateFilter(ctrl.activeDate, ctrl.formatMonthTitle); - scope.rows = ctrl.split(months, 3); - }; - - ctrl.compare = function(date1, date2) { - return new Date( date1.getFullYear(), date1.getMonth() ) - new Date( date2.getFullYear(), date2.getMonth() ); - }; - - ctrl.handleKeyDown = function( key, evt ) { - var date = ctrl.activeDate.getMonth(); - - if (key === 'left') { - date = date - 1; // up - } else if (key === 'up') { - date = date - 3; // down - } else if (key === 'right') { - date = date + 1; // down - } else if (key === 'down') { - date = date + 3; - } else if (key === 'pageup' || key === 'pagedown') { - var year = ctrl.activeDate.getFullYear() + (key === 'pageup' ? - 1 : 1); - ctrl.activeDate.setFullYear(year); - } else if (key === 'home') { - date = 0; - } else if (key === 'end') { - date = 11; - } - ctrl.activeDate.setMonth(date); - }; - - ctrl.refreshView(); - } - }; -}]) - -.directive('yearpicker', ['dateFilter', function (dateFilter) { - return { - restrict: 'EA', - replace: true, - templateUrl: 'template/datepicker/year.html', - require: '^datepicker', - link: function(scope, element, attrs, ctrl) { - var range = ctrl.yearRange; - - ctrl.step = { years: range }; - ctrl.element = element; - - function getStartingYear( year ) { - return parseInt((year - 1) / range, 10) * range + 1; - } - - ctrl._refreshView = function() { - var years = new Array(range); - - for ( var i = 0, start = getStartingYear(ctrl.activeDate.getFullYear()); i < range; i++ ) { - years[i] = angular.extend(ctrl.createDateObject(new Date(start + i, 0, 1), ctrl.formatYear), { - uid: scope.uniqueId + '-' + i - }); - } - - scope.title = [years[0].label, years[range - 1].label].join(' - '); - scope.rows = ctrl.split(years, 5); - }; - - ctrl.compare = function(date1, date2) { - return date1.getFullYear() - date2.getFullYear(); - }; - - ctrl.handleKeyDown = function( key, evt ) { - var date = ctrl.activeDate.getFullYear(); - - if (key === 'left') { - date = date - 1; // up - } else if (key === 'up') { - date = date - 5; // down - } else if (key === 'right') { - date = date + 1; // down - } else if (key === 'down') { - date = date + 5; - } else if (key === 'pageup' || key === 'pagedown') { - date += (key === 'pageup' ? - 1 : 1) * ctrl.step.years; - } else if (key === 'home') { - date = getStartingYear( ctrl.activeDate.getFullYear() ); - } else if (key === 'end') { - date = getStartingYear( ctrl.activeDate.getFullYear() ) + range - 1; - } - ctrl.activeDate.setFullYear(date); - }; - - ctrl.refreshView(); - } - }; -}]) - -.constant('datepickerPopupConfig', { - datepickerPopup: 'yyyy-MM-dd', - currentText: 'Today', - clearText: 'Clear', - closeText: 'Done', - closeOnDateSelection: true, - appendToBody: false, - showButtonBar: true -}) - -.directive('datepickerPopup', ['$compile', '$parse', '$document', '$position', 'dateFilter', 'dateParser', 'datepickerPopupConfig', -function ($compile, $parse, $document, $position, dateFilter, dateParser, datepickerPopupConfig) { - return { - restrict: 'EA', - require: 'ngModel', - scope: { - isOpen: '=?', - currentText: '@', - clearText: '@', - closeText: '@', - dateDisabled: '&' + templateUrl: function(element, attrs) { + return attrs.templateUrl || 'uib/template/datepicker/day.html'; }, - link: function(scope, element, attrs, ngModel) { - var dateFormat, - closeOnDateSelection = angular.isDefined(attrs.closeOnDateSelection) ? scope.$parent.$eval(attrs.closeOnDateSelection) : datepickerPopupConfig.closeOnDateSelection, - appendToBody = angular.isDefined(attrs.datepickerAppendToBody) ? scope.$parent.$eval(attrs.datepickerAppendToBody) : datepickerPopupConfig.appendToBody; + require: ['^uibDatepicker', 'uibDaypicker'], + controller: 'UibDaypickerController', + link: function(scope, element, attrs, ctrls) { + var datepickerCtrl = ctrls[0], + daypickerCtrl = ctrls[1]; - scope.showButtonBar = angular.isDefined(attrs.showButtonBar) ? scope.$parent.$eval(attrs.showButtonBar) : datepickerPopupConfig.showButtonBar; - - scope.getText = function( key ) { - return scope[key + 'Text'] || datepickerPopupConfig[key + 'Text']; - }; - - attrs.$observe('datepickerPopup', function(value) { - dateFormat = value || datepickerPopupConfig.datepickerPopup; - ngModel.$render(); - }); - - // popup element used to display calendar - var popupEl = angular.element('
'); - popupEl.attr({ - 'ng-model': 'date', - 'ng-change': 'dateSelection()' - }); - - function cameltoDash( string ){ - return string.replace(/([A-Z])/g, function($1) { return '-' + $1.toLowerCase(); }); - } - - // datepicker element - var datepickerEl = angular.element(popupEl.children()[0]); - if ( attrs.datepickerOptions ) { - angular.forEach(scope.$parent.$eval(attrs.datepickerOptions), function( value, option ) { - datepickerEl.attr( cameltoDash(option), value ); - }); - } - - angular.forEach(['minDate', 'maxDate'], function( key ) { - if ( attrs[key] ) { - scope.$parent.$watch($parse(attrs[key]), function(value){ - scope[key] = value; - }); - datepickerEl.attr(cameltoDash(key), key); - } - }); - if (attrs.dateDisabled) { - datepickerEl.attr('date-disabled', 'dateDisabled({ date: date, mode: mode })'); - } - - function parseDate(viewValue) { - if (!viewValue) { - ngModel.$setValidity('date', true); - return null; - } else if (angular.isDate(viewValue) && !isNaN(viewValue)) { - ngModel.$setValidity('date', true); - return viewValue; - } else if (angular.isString(viewValue)) { - var date = dateParser.parse(viewValue, dateFormat) || new Date(viewValue); - if (isNaN(date)) { - ngModel.$setValidity('date', false); - return undefined; - } else { - ngModel.$setValidity('date', true); - return date; - } - } else { - ngModel.$setValidity('date', false); - return undefined; - } - } - ngModel.$parsers.unshift(parseDate); - - // Inner change - scope.dateSelection = function(dt) { - if (angular.isDefined(dt)) { - scope.date = dt; - } - ngModel.$setViewValue(scope.date); - ngModel.$render(); - - if ( closeOnDateSelection ) { - scope.isOpen = false; - element[0].focus(); - } - }; - - element.bind('input change keyup', function() { - scope.$apply(function() { - scope.date = ngModel.$modelValue; - }); - }); - - // Outter change - ngModel.$render = function() { - var date = ngModel.$viewValue ? dateFilter(ngModel.$viewValue, dateFormat) : ''; - element.val(date); - scope.date = parseDate( ngModel.$modelValue ); - }; - - var documentClickBind = function(event) { - if (scope.isOpen && event.target !== element[0]) { - scope.$apply(function() { - scope.isOpen = false; - }); - } - }; - - var keydown = function(evt, noApply) { - scope.keydown(evt); - }; - element.bind('keydown', keydown); - - scope.keydown = function(evt) { - if (evt.which === 27) { - evt.preventDefault(); - evt.stopPropagation(); - scope.close(); - } else if (evt.which === 40 && !scope.isOpen) { - scope.isOpen = true; - } - }; - - scope.$watch('isOpen', function(value) { - if (value) { - scope.$broadcast('datepicker.focus'); - scope.position = appendToBody ? $position.offset(element) : $position.position(element); - scope.position.top = scope.position.top + element.prop('offsetHeight'); - - $document.bind('click', documentClickBind); - } else { - $document.unbind('click', documentClickBind); - } - }); - - scope.select = function( date ) { - if (date === 'today') { - var today = new Date(); - if (angular.isDate(ngModel.$modelValue)) { - date = new Date(ngModel.$modelValue); - date.setFullYear(today.getFullYear(), today.getMonth(), today.getDate()); - } else { - date = new Date(today.setHours(0, 0, 0, 0)); - } - } - scope.dateSelection( date ); - }; - - scope.close = function() { - scope.isOpen = false; - element[0].focus(); - }; - - var $popup = $compile(popupEl)(scope); - if ( appendToBody ) { - $document.find('body').append($popup); - } else { - element.after($popup); - } - - scope.$on('$destroy', function() { - $popup.remove(); - element.unbind('keydown', keydown); - $document.unbind('click', documentClickBind); - }); + daypickerCtrl.init(datepickerCtrl); } }; -}]) +}) -.directive('datepickerPopupWrap', function() { +.directive('uibMonthpicker', function() { return { - restrict:'EA', replace: true, - transclude: true, - templateUrl: 'template/datepicker/popup.html', - link:function (scope, element, attrs) { - element.bind('click', function(event) { - event.preventDefault(); - event.stopPropagation(); - }); + templateUrl: function(element, attrs) { + return attrs.templateUrl || 'uib/template/datepicker/month.html'; + }, + require: ['^uibDatepicker', 'uibMonthpicker'], + controller: 'UibMonthpickerController', + link: function(scope, element, attrs, ctrls) { + var datepickerCtrl = ctrls[0], + monthpickerCtrl = ctrls[1]; + + monthpickerCtrl.init(datepickerCtrl); + } + }; +}) + +.directive('uibYearpicker', function() { + return { + replace: true, + templateUrl: function(element, attrs) { + return attrs.templateUrl || 'uib/template/datepicker/year.html'; + }, + require: ['^uibDatepicker', 'uibYearpicker'], + controller: 'UibYearpickerController', + link: function(scope, element, attrs, ctrls) { + var ctrl = ctrls[0]; + angular.extend(ctrl, ctrls[1]); + ctrl.yearpickerInit(); + + ctrl.refreshView(); } }; }); -angular.module('ui.bootstrap.dropdown', []) +angular.module('ui.bootstrap.position', []) -.constant('dropdownConfig', { +/** + * A set of utility methods for working with the DOM. + * It is meant to be used where we need to absolute-position elements in + * relation to another element (this is the case for tooltips, popovers, + * typeahead suggestions etc.). + */ + .factory('$uibPosition', ['$document', '$window', function($document, $window) { + /** + * Used by scrollbarWidth() function to cache scrollbar's width. + * Do not access this variable directly, use scrollbarWidth() instead. + */ + var SCROLLBAR_WIDTH; + /** + * scrollbar on body and html element in IE and Edge overlay + * content and should be considered 0 width. + */ + var BODY_SCROLLBAR_WIDTH; + var OVERFLOW_REGEX = { + normal: /(auto|scroll)/, + hidden: /(auto|scroll|hidden)/ + }; + var PLACEMENT_REGEX = { + auto: /\s?auto?\s?/i, + primary: /^(top|bottom|left|right)$/, + secondary: /^(top|bottom|left|right|center)$/, + vertical: /^(top|bottom)$/ + }; + var BODY_REGEX = /(HTML|BODY)/; + + return { + + /** + * Provides a raw DOM element from a jQuery/jQLite element. + * + * @param {element} elem - The element to convert. + * + * @returns {element} A HTML element. + */ + getRawNode: function(elem) { + return elem.nodeName ? elem : elem[0] || elem; + }, + + /** + * Provides a parsed number for a style property. Strips + * units and casts invalid numbers to 0. + * + * @param {string} value - The style value to parse. + * + * @returns {number} A valid number. + */ + parseStyle: function(value) { + value = parseFloat(value); + return isFinite(value) ? value : 0; + }, + + /** + * Provides the closest positioned ancestor. + * + * @param {element} element - The element to get the offest parent for. + * + * @returns {element} The closest positioned ancestor. + */ + offsetParent: function(elem) { + elem = this.getRawNode(elem); + + var offsetParent = elem.offsetParent || $document[0].documentElement; + + function isStaticPositioned(el) { + return ($window.getComputedStyle(el).position || 'static') === 'static'; + } + + while (offsetParent && offsetParent !== $document[0].documentElement && isStaticPositioned(offsetParent)) { + offsetParent = offsetParent.offsetParent; + } + + return offsetParent || $document[0].documentElement; + }, + + /** + * Provides the scrollbar width, concept from TWBS measureScrollbar() + * function in https://github.com/twbs/bootstrap/blob/master/js/modal.js + * In IE and Edge, scollbar on body and html element overlay and should + * return a width of 0. + * + * @returns {number} The width of the browser scollbar. + */ + scrollbarWidth: function(isBody) { + if (isBody) { + if (angular.isUndefined(BODY_SCROLLBAR_WIDTH)) { + var bodyElem = $document.find('body'); + bodyElem.addClass('uib-position-body-scrollbar-measure'); + BODY_SCROLLBAR_WIDTH = $window.innerWidth - bodyElem[0].clientWidth; + BODY_SCROLLBAR_WIDTH = isFinite(BODY_SCROLLBAR_WIDTH) ? BODY_SCROLLBAR_WIDTH : 0; + bodyElem.removeClass('uib-position-body-scrollbar-measure'); + } + return BODY_SCROLLBAR_WIDTH; + } + + if (angular.isUndefined(SCROLLBAR_WIDTH)) { + var scrollElem = angular.element('
'); + $document.find('body').append(scrollElem); + SCROLLBAR_WIDTH = scrollElem[0].offsetWidth - scrollElem[0].clientWidth; + SCROLLBAR_WIDTH = isFinite(SCROLLBAR_WIDTH) ? SCROLLBAR_WIDTH : 0; + scrollElem.remove(); + } + + return SCROLLBAR_WIDTH; + }, + + /** + * Provides the padding required on an element to replace the scrollbar. + * + * @returns {object} An object with the following properties: + * + */ + scrollbarPadding: function(elem) { + elem = this.getRawNode(elem); + + var elemStyle = $window.getComputedStyle(elem); + var paddingRight = this.parseStyle(elemStyle.paddingRight); + var paddingBottom = this.parseStyle(elemStyle.paddingBottom); + var scrollParent = this.scrollParent(elem, false, true); + var scrollbarWidth = this.scrollbarWidth(scrollParent, BODY_REGEX.test(scrollParent.tagName)); + + return { + scrollbarWidth: scrollbarWidth, + widthOverflow: scrollParent.scrollWidth > scrollParent.clientWidth, + right: paddingRight + scrollbarWidth, + originalRight: paddingRight, + heightOverflow: scrollParent.scrollHeight > scrollParent.clientHeight, + bottom: paddingBottom + scrollbarWidth, + originalBottom: paddingBottom + }; + }, + + /** + * Checks to see if the element is scrollable. + * + * @param {element} elem - The element to check. + * @param {boolean=} [includeHidden=false] - Should scroll style of 'hidden' be considered, + * default is false. + * + * @returns {boolean} Whether the element is scrollable. + */ + isScrollable: function(elem, includeHidden) { + elem = this.getRawNode(elem); + + var overflowRegex = includeHidden ? OVERFLOW_REGEX.hidden : OVERFLOW_REGEX.normal; + var elemStyle = $window.getComputedStyle(elem); + return overflowRegex.test(elemStyle.overflow + elemStyle.overflowY + elemStyle.overflowX); + }, + + /** + * Provides the closest scrollable ancestor. + * A port of the jQuery UI scrollParent method: + * https://github.com/jquery/jquery-ui/blob/master/ui/scroll-parent.js + * + * @param {element} elem - The element to find the scroll parent of. + * @param {boolean=} [includeHidden=false] - Should scroll style of 'hidden' be considered, + * default is false. + * @param {boolean=} [includeSelf=false] - Should the element being passed be + * included in the scrollable llokup. + * + * @returns {element} A HTML element. + */ + scrollParent: function(elem, includeHidden, includeSelf) { + elem = this.getRawNode(elem); + + var overflowRegex = includeHidden ? OVERFLOW_REGEX.hidden : OVERFLOW_REGEX.normal; + var documentEl = $document[0].documentElement; + var elemStyle = $window.getComputedStyle(elem); + if (includeSelf && overflowRegex.test(elemStyle.overflow + elemStyle.overflowY + elemStyle.overflowX)) { + return elem; + } + var excludeStatic = elemStyle.position === 'absolute'; + var scrollParent = elem.parentElement || documentEl; + + if (scrollParent === documentEl || elemStyle.position === 'fixed') { + return documentEl; + } + + while (scrollParent.parentElement && scrollParent !== documentEl) { + var spStyle = $window.getComputedStyle(scrollParent); + if (excludeStatic && spStyle.position !== 'static') { + excludeStatic = false; + } + + if (!excludeStatic && overflowRegex.test(spStyle.overflow + spStyle.overflowY + spStyle.overflowX)) { + break; + } + scrollParent = scrollParent.parentElement; + } + + return scrollParent; + }, + + /** + * Provides read-only equivalent of jQuery's position function: + * http://api.jquery.com/position/ - distance to closest positioned + * ancestor. Does not account for margins by default like jQuery position. + * + * @param {element} elem - The element to caclulate the position on. + * @param {boolean=} [includeMargins=false] - Should margins be accounted + * for, default is false. + * + * @returns {object} An object with the following properties: + * + */ + position: function(elem, includeMagins) { + elem = this.getRawNode(elem); + + var elemOffset = this.offset(elem); + if (includeMagins) { + var elemStyle = $window.getComputedStyle(elem); + elemOffset.top -= this.parseStyle(elemStyle.marginTop); + elemOffset.left -= this.parseStyle(elemStyle.marginLeft); + } + var parent = this.offsetParent(elem); + var parentOffset = {top: 0, left: 0}; + + if (parent !== $document[0].documentElement) { + parentOffset = this.offset(parent); + parentOffset.top += parent.clientTop - parent.scrollTop; + parentOffset.left += parent.clientLeft - parent.scrollLeft; + } + + return { + width: Math.round(angular.isNumber(elemOffset.width) ? elemOffset.width : elem.offsetWidth), + height: Math.round(angular.isNumber(elemOffset.height) ? elemOffset.height : elem.offsetHeight), + top: Math.round(elemOffset.top - parentOffset.top), + left: Math.round(elemOffset.left - parentOffset.left) + }; + }, + + /** + * Provides read-only equivalent of jQuery's offset function: + * http://api.jquery.com/offset/ - distance to viewport. Does + * not account for borders, margins, or padding on the body + * element. + * + * @param {element} elem - The element to calculate the offset on. + * + * @returns {object} An object with the following properties: + * + */ + offset: function(elem) { + elem = this.getRawNode(elem); + + var elemBCR = elem.getBoundingClientRect(); + return { + width: Math.round(angular.isNumber(elemBCR.width) ? elemBCR.width : elem.offsetWidth), + height: Math.round(angular.isNumber(elemBCR.height) ? elemBCR.height : elem.offsetHeight), + top: Math.round(elemBCR.top + ($window.pageYOffset || $document[0].documentElement.scrollTop)), + left: Math.round(elemBCR.left + ($window.pageXOffset || $document[0].documentElement.scrollLeft)) + }; + }, + + /** + * Provides offset distance to the closest scrollable ancestor + * or viewport. Accounts for border and scrollbar width. + * + * Right and bottom dimensions represent the distance to the + * respective edge of the viewport element. If the element + * edge extends beyond the viewport, a negative value will be + * reported. + * + * @param {element} elem - The element to get the viewport offset for. + * @param {boolean=} [useDocument=false] - Should the viewport be the document element instead + * of the first scrollable element, default is false. + * @param {boolean=} [includePadding=true] - Should the padding on the offset parent element + * be accounted for, default is true. + * + * @returns {object} An object with the following properties: + * + */ + viewportOffset: function(elem, useDocument, includePadding) { + elem = this.getRawNode(elem); + includePadding = includePadding !== false ? true : false; + + var elemBCR = elem.getBoundingClientRect(); + var offsetBCR = {top: 0, left: 0, bottom: 0, right: 0}; + + var offsetParent = useDocument ? $document[0].documentElement : this.scrollParent(elem); + var offsetParentBCR = offsetParent.getBoundingClientRect(); + + offsetBCR.top = offsetParentBCR.top + offsetParent.clientTop; + offsetBCR.left = offsetParentBCR.left + offsetParent.clientLeft; + if (offsetParent === $document[0].documentElement) { + offsetBCR.top += $window.pageYOffset; + offsetBCR.left += $window.pageXOffset; + } + offsetBCR.bottom = offsetBCR.top + offsetParent.clientHeight; + offsetBCR.right = offsetBCR.left + offsetParent.clientWidth; + + if (includePadding) { + var offsetParentStyle = $window.getComputedStyle(offsetParent); + offsetBCR.top += this.parseStyle(offsetParentStyle.paddingTop); + offsetBCR.bottom -= this.parseStyle(offsetParentStyle.paddingBottom); + offsetBCR.left += this.parseStyle(offsetParentStyle.paddingLeft); + offsetBCR.right -= this.parseStyle(offsetParentStyle.paddingRight); + } + + return { + top: Math.round(elemBCR.top - offsetBCR.top), + bottom: Math.round(offsetBCR.bottom - elemBCR.bottom), + left: Math.round(elemBCR.left - offsetBCR.left), + right: Math.round(offsetBCR.right - elemBCR.right) + }; + }, + + /** + * Provides an array of placement values parsed from a placement string. + * Along with the 'auto' indicator, supported placement strings are: + * + * A placement string with an 'auto' indicator is expected to be + * space separated from the placement, i.e: 'auto bottom-left' If + * the primary and secondary placement values do not match 'top, + * bottom, left, right' then 'top' will be the primary placement and + * 'center' will be the secondary placement. If 'auto' is passed, true + * will be returned as the 3rd value of the array. + * + * @param {string} placement - The placement string to parse. + * + * @returns {array} An array with the following values + * + */ + parsePlacement: function(placement) { + var autoPlace = PLACEMENT_REGEX.auto.test(placement); + if (autoPlace) { + placement = placement.replace(PLACEMENT_REGEX.auto, ''); + } + + placement = placement.split('-'); + + placement[0] = placement[0] || 'top'; + if (!PLACEMENT_REGEX.primary.test(placement[0])) { + placement[0] = 'top'; + } + + placement[1] = placement[1] || 'center'; + if (!PLACEMENT_REGEX.secondary.test(placement[1])) { + placement[1] = 'center'; + } + + if (autoPlace) { + placement[2] = true; + } else { + placement[2] = false; + } + + return placement; + }, + + /** + * Provides coordinates for an element to be positioned relative to + * another element. Passing 'auto' as part of the placement parameter + * will enable smart placement - where the element fits. i.e: + * 'auto left-top' will check to see if there is enough space to the left + * of the hostElem to fit the targetElem, if not place right (same for secondary + * top placement). Available space is calculated using the viewportOffset + * function. + * + * @param {element} hostElem - The element to position against. + * @param {element} targetElem - The element to position. + * @param {string=} [placement=top] - The placement for the targetElem, + * default is 'top'. 'center' is assumed as secondary placement for + * 'top', 'left', 'right', and 'bottom' placements. Available placements are: + * + * @param {boolean=} [appendToBody=false] - Should the top and left values returned + * be calculated from the body element, default is false. + * + * @returns {object} An object with the following properties: + * + */ + positionElements: function(hostElem, targetElem, placement, appendToBody) { + hostElem = this.getRawNode(hostElem); + targetElem = this.getRawNode(targetElem); + + // need to read from prop to support tests. + var targetWidth = angular.isDefined(targetElem.offsetWidth) ? targetElem.offsetWidth : targetElem.prop('offsetWidth'); + var targetHeight = angular.isDefined(targetElem.offsetHeight) ? targetElem.offsetHeight : targetElem.prop('offsetHeight'); + + placement = this.parsePlacement(placement); + + var hostElemPos = appendToBody ? this.offset(hostElem) : this.position(hostElem); + var targetElemPos = {top: 0, left: 0, placement: ''}; + + if (placement[2]) { + var viewportOffset = this.viewportOffset(hostElem, appendToBody); + + var targetElemStyle = $window.getComputedStyle(targetElem); + var adjustedSize = { + width: targetWidth + Math.round(Math.abs(this.parseStyle(targetElemStyle.marginLeft) + this.parseStyle(targetElemStyle.marginRight))), + height: targetHeight + Math.round(Math.abs(this.parseStyle(targetElemStyle.marginTop) + this.parseStyle(targetElemStyle.marginBottom))) + }; + + placement[0] = placement[0] === 'top' && adjustedSize.height > viewportOffset.top && adjustedSize.height <= viewportOffset.bottom ? 'bottom' : + placement[0] === 'bottom' && adjustedSize.height > viewportOffset.bottom && adjustedSize.height <= viewportOffset.top ? 'top' : + placement[0] === 'left' && adjustedSize.width > viewportOffset.left && adjustedSize.width <= viewportOffset.right ? 'right' : + placement[0] === 'right' && adjustedSize.width > viewportOffset.right && adjustedSize.width <= viewportOffset.left ? 'left' : + placement[0]; + + placement[1] = placement[1] === 'top' && adjustedSize.height - hostElemPos.height > viewportOffset.bottom && adjustedSize.height - hostElemPos.height <= viewportOffset.top ? 'bottom' : + placement[1] === 'bottom' && adjustedSize.height - hostElemPos.height > viewportOffset.top && adjustedSize.height - hostElemPos.height <= viewportOffset.bottom ? 'top' : + placement[1] === 'left' && adjustedSize.width - hostElemPos.width > viewportOffset.right && adjustedSize.width - hostElemPos.width <= viewportOffset.left ? 'right' : + placement[1] === 'right' && adjustedSize.width - hostElemPos.width > viewportOffset.left && adjustedSize.width - hostElemPos.width <= viewportOffset.right ? 'left' : + placement[1]; + + if (placement[1] === 'center') { + if (PLACEMENT_REGEX.vertical.test(placement[0])) { + var xOverflow = hostElemPos.width / 2 - targetWidth / 2; + if (viewportOffset.left + xOverflow < 0 && adjustedSize.width - hostElemPos.width <= viewportOffset.right) { + placement[1] = 'left'; + } else if (viewportOffset.right + xOverflow < 0 && adjustedSize.width - hostElemPos.width <= viewportOffset.left) { + placement[1] = 'right'; + } + } else { + var yOverflow = hostElemPos.height / 2 - adjustedSize.height / 2; + if (viewportOffset.top + yOverflow < 0 && adjustedSize.height - hostElemPos.height <= viewportOffset.bottom) { + placement[1] = 'top'; + } else if (viewportOffset.bottom + yOverflow < 0 && adjustedSize.height - hostElemPos.height <= viewportOffset.top) { + placement[1] = 'bottom'; + } + } + } + } + + switch (placement[0]) { + case 'top': + targetElemPos.top = hostElemPos.top - targetHeight; + break; + case 'bottom': + targetElemPos.top = hostElemPos.top + hostElemPos.height; + break; + case 'left': + targetElemPos.left = hostElemPos.left - targetWidth; + break; + case 'right': + targetElemPos.left = hostElemPos.left + hostElemPos.width; + break; + } + + switch (placement[1]) { + case 'top': + targetElemPos.top = hostElemPos.top; + break; + case 'bottom': + targetElemPos.top = hostElemPos.top + hostElemPos.height - targetHeight; + break; + case 'left': + targetElemPos.left = hostElemPos.left; + break; + case 'right': + targetElemPos.left = hostElemPos.left + hostElemPos.width - targetWidth; + break; + case 'center': + if (PLACEMENT_REGEX.vertical.test(placement[0])) { + targetElemPos.left = hostElemPos.left + hostElemPos.width / 2 - targetWidth / 2; + } else { + targetElemPos.top = hostElemPos.top + hostElemPos.height / 2 - targetHeight / 2; + } + break; + } + + targetElemPos.top = Math.round(targetElemPos.top); + targetElemPos.left = Math.round(targetElemPos.left); + targetElemPos.placement = placement[1] === 'center' ? placement[0] : placement[0] + '-' + placement[1]; + + return targetElemPos; + }, + + /** + * Provides a way for positioning tooltip & dropdown + * arrows when using placement options beyond the standard + * left, right, top, or bottom. + * + * @param {element} elem - The tooltip/dropdown element. + * @param {string} placement - The placement for the elem. + */ + positionArrow: function(elem, placement) { + elem = this.getRawNode(elem); + + var innerElem = elem.querySelector('.tooltip-inner, .popover-inner'); + if (!innerElem) { + return; + } + + var isTooltip = angular.element(innerElem).hasClass('tooltip-inner'); + + var arrowElem = isTooltip ? elem.querySelector('.tooltip-arrow') : elem.querySelector('.arrow'); + if (!arrowElem) { + return; + } + + var arrowCss = { + top: '', + bottom: '', + left: '', + right: '' + }; + + placement = this.parsePlacement(placement); + if (placement[1] === 'center') { + // no adjustment necessary - just reset styles + angular.element(arrowElem).css(arrowCss); + return; + } + + var borderProp = 'border-' + placement[0] + '-width'; + var borderWidth = $window.getComputedStyle(arrowElem)[borderProp]; + + var borderRadiusProp = 'border-'; + if (PLACEMENT_REGEX.vertical.test(placement[0])) { + borderRadiusProp += placement[0] + '-' + placement[1]; + } else { + borderRadiusProp += placement[1] + '-' + placement[0]; + } + borderRadiusProp += '-radius'; + var borderRadius = $window.getComputedStyle(isTooltip ? innerElem : elem)[borderRadiusProp]; + + switch (placement[0]) { + case 'top': + arrowCss.bottom = isTooltip ? '0' : '-' + borderWidth; + break; + case 'bottom': + arrowCss.top = isTooltip ? '0' : '-' + borderWidth; + break; + case 'left': + arrowCss.right = isTooltip ? '0' : '-' + borderWidth; + break; + case 'right': + arrowCss.left = isTooltip ? '0' : '-' + borderWidth; + break; + } + + arrowCss[placement[1]] = borderRadius; + + angular.element(arrowElem).css(arrowCss); + } + }; + }]); + +angular.module('ui.bootstrap.datepickerPopup', ['ui.bootstrap.datepicker', 'ui.bootstrap.position']) + +.value('$datepickerPopupLiteralWarning', true) + +.constant('uibDatepickerPopupConfig', { + altInputFormats: [], + appendToBody: false, + clearText: 'Clear', + closeOnDateSelection: true, + closeText: 'Done', + currentText: 'Today', + datepickerPopup: 'yyyy-MM-dd', + datepickerPopupTemplateUrl: 'uib/template/datepickerPopup/popup.html', + datepickerTemplateUrl: 'uib/template/datepicker/datepicker.html', + html5Types: { + date: 'yyyy-MM-dd', + 'datetime-local': 'yyyy-MM-ddTHH:mm:ss.sss', + 'month': 'yyyy-MM' + }, + onOpenFocus: true, + showButtonBar: true, + placement: 'auto bottom-left' +}) + +.controller('UibDatepickerPopupController', ['$scope', '$element', '$attrs', '$compile', '$log', '$parse', '$window', '$document', '$rootScope', '$uibPosition', 'dateFilter', 'uibDateParser', 'uibDatepickerPopupConfig', '$timeout', 'uibDatepickerConfig', '$datepickerPopupLiteralWarning', +function($scope, $element, $attrs, $compile, $log, $parse, $window, $document, $rootScope, $position, dateFilter, dateParser, datepickerPopupConfig, $timeout, datepickerConfig, $datepickerPopupLiteralWarning) { + var cache = {}, + isHtml5DateInput = false; + var dateFormat, closeOnDateSelection, appendToBody, onOpenFocus, + datepickerPopupTemplateUrl, datepickerTemplateUrl, popupEl, datepickerEl, scrollParentEl, + ngModel, ngModelOptions, $popup, altInputFormats, watchListeners = [], + timezone; + + this.init = function(_ngModel_) { + ngModel = _ngModel_; + ngModelOptions = _ngModel_.$options; + closeOnDateSelection = angular.isDefined($attrs.closeOnDateSelection) ? + $scope.$parent.$eval($attrs.closeOnDateSelection) : + datepickerPopupConfig.closeOnDateSelection; + appendToBody = angular.isDefined($attrs.datepickerAppendToBody) ? + $scope.$parent.$eval($attrs.datepickerAppendToBody) : + datepickerPopupConfig.appendToBody; + onOpenFocus = angular.isDefined($attrs.onOpenFocus) ? + $scope.$parent.$eval($attrs.onOpenFocus) : datepickerPopupConfig.onOpenFocus; + datepickerPopupTemplateUrl = angular.isDefined($attrs.datepickerPopupTemplateUrl) ? + $attrs.datepickerPopupTemplateUrl : + datepickerPopupConfig.datepickerPopupTemplateUrl; + datepickerTemplateUrl = angular.isDefined($attrs.datepickerTemplateUrl) ? + $attrs.datepickerTemplateUrl : datepickerPopupConfig.datepickerTemplateUrl; + altInputFormats = angular.isDefined($attrs.altInputFormats) ? + $scope.$parent.$eval($attrs.altInputFormats) : + datepickerPopupConfig.altInputFormats; + + $scope.showButtonBar = angular.isDefined($attrs.showButtonBar) ? + $scope.$parent.$eval($attrs.showButtonBar) : + datepickerPopupConfig.showButtonBar; + + if (datepickerPopupConfig.html5Types[$attrs.type]) { + dateFormat = datepickerPopupConfig.html5Types[$attrs.type]; + isHtml5DateInput = true; + } else { + dateFormat = $attrs.uibDatepickerPopup || datepickerPopupConfig.datepickerPopup; + $attrs.$observe('uibDatepickerPopup', function(value, oldValue) { + var newDateFormat = value || datepickerPopupConfig.datepickerPopup; + // Invalidate the $modelValue to ensure that formatters re-run + // FIXME: Refactor when PR is merged: https://github.com/angular/angular.js/pull/10764 + if (newDateFormat !== dateFormat) { + dateFormat = newDateFormat; + ngModel.$modelValue = null; + + if (!dateFormat) { + throw new Error('uibDatepickerPopup must have a date format specified.'); + } + } + }); + } + + if (!dateFormat) { + throw new Error('uibDatepickerPopup must have a date format specified.'); + } + + if (isHtml5DateInput && $attrs.uibDatepickerPopup) { + throw new Error('HTML5 date input types do not support custom formats.'); + } + + // popup element used to display calendar + popupEl = angular.element('
'); + if (ngModelOptions) { + timezone = ngModelOptions.timezone; + $scope.ngModelOptions = angular.copy(ngModelOptions); + $scope.ngModelOptions.timezone = null; + if ($scope.ngModelOptions.updateOnDefault === true) { + $scope.ngModelOptions.updateOn = $scope.ngModelOptions.updateOn ? + $scope.ngModelOptions.updateOn + ' default' : 'default'; + } + + popupEl.attr('ng-model-options', 'ngModelOptions'); + } else { + timezone = null; + } + + popupEl.attr({ + 'ng-model': 'date', + 'ng-change': 'dateSelection(date)', + 'template-url': datepickerPopupTemplateUrl + }); + + // datepicker element + datepickerEl = angular.element(popupEl.children()[0]); + datepickerEl.attr('template-url', datepickerTemplateUrl); + + if (!$scope.datepickerOptions) { + $scope.datepickerOptions = {}; + } + + if (isHtml5DateInput) { + if ($attrs.type === 'month') { + $scope.datepickerOptions.datepickerMode = 'month'; + $scope.datepickerOptions.minMode = 'month'; + } + } + + datepickerEl.attr('datepicker-options', 'datepickerOptions'); + + if (!isHtml5DateInput) { + // Internal API to maintain the correct ng-invalid-[key] class + ngModel.$$parserName = 'date'; + ngModel.$validators.date = validator; + ngModel.$parsers.unshift(parseDate); + ngModel.$formatters.push(function(value) { + if (ngModel.$isEmpty(value)) { + $scope.date = value; + return value; + } + + $scope.date = dateParser.fromTimezone(value, timezone); + + if (angular.isNumber($scope.date)) { + $scope.date = new Date($scope.date); + } + + return dateParser.filter($scope.date, dateFormat); + }); + } else { + ngModel.$formatters.push(function(value) { + $scope.date = dateParser.fromTimezone(value, timezone); + return value; + }); + } + + // Detect changes in the view from the text box + ngModel.$viewChangeListeners.push(function() { + $scope.date = parseDateString(ngModel.$viewValue); + }); + + $element.on('keydown', inputKeydownBind); + + $popup = $compile(popupEl)($scope); + // Prevent jQuery cache memory leak (template is now redundant after linking) + popupEl.remove(); + + if (appendToBody) { + $document.find('body').append($popup); + } else { + $element.after($popup); + } + + $scope.$on('$destroy', function() { + if ($scope.isOpen === true) { + if (!$rootScope.$$phase) { + $scope.$apply(function() { + $scope.isOpen = false; + }); + } + } + + $popup.remove(); + $element.off('keydown', inputKeydownBind); + $document.off('click', documentClickBind); + if (scrollParentEl) { + scrollParentEl.off('scroll', positionPopup); + } + angular.element($window).off('resize', positionPopup); + + //Clear all watch listeners on destroy + while (watchListeners.length) { + watchListeners.shift()(); + } + }); + }; + + $scope.getText = function(key) { + return $scope[key + 'Text'] || datepickerPopupConfig[key + 'Text']; + }; + + $scope.isDisabled = function(date) { + if (date === 'today') { + date = dateParser.fromTimezone(new Date(), timezone); + } + + var dates = {}; + angular.forEach(['minDate', 'maxDate'], function(key) { + if (!$scope.datepickerOptions[key]) { + dates[key] = null; + } else if (angular.isDate($scope.datepickerOptions[key])) { + dates[key] = dateParser.fromTimezone(new Date($scope.datepickerOptions[key]), timezone); + } else { + if ($datepickerPopupLiteralWarning) { + $log.warn('Literal date support has been deprecated, please switch to date object usage'); + } + + dates[key] = new Date(dateFilter($scope.datepickerOptions[key], 'medium')); + } + }); + + return $scope.datepickerOptions && + dates.minDate && $scope.compare(date, dates.minDate) < 0 || + dates.maxDate && $scope.compare(date, dates.maxDate) > 0; + }; + + $scope.compare = function(date1, date2) { + return new Date(date1.getFullYear(), date1.getMonth(), date1.getDate()) - new Date(date2.getFullYear(), date2.getMonth(), date2.getDate()); + }; + + // Inner change + $scope.dateSelection = function(dt) { + if (angular.isDefined(dt)) { + $scope.date = dt; + } + var date = $scope.date ? dateParser.filter($scope.date, dateFormat) : null; // Setting to NULL is necessary for form validators to function + $element.val(date); + ngModel.$setViewValue(date); + + if (closeOnDateSelection) { + $scope.isOpen = false; + $element[0].focus(); + } + }; + + $scope.keydown = function(evt) { + if (evt.which === 27) { + evt.stopPropagation(); + $scope.isOpen = false; + $element[0].focus(); + } + }; + + $scope.select = function(date, evt) { + evt.stopPropagation(); + + if (date === 'today') { + var today = new Date(); + if (angular.isDate($scope.date)) { + date = new Date($scope.date); + date.setFullYear(today.getFullYear(), today.getMonth(), today.getDate()); + } else { + date = new Date(today.setHours(0, 0, 0, 0)); + } + } + $scope.dateSelection(date); + }; + + $scope.close = function(evt) { + evt.stopPropagation(); + + $scope.isOpen = false; + $element[0].focus(); + }; + + $scope.disabled = angular.isDefined($attrs.disabled) || false; + if ($attrs.ngDisabled) { + watchListeners.push($scope.$parent.$watch($parse($attrs.ngDisabled), function(disabled) { + $scope.disabled = disabled; + })); + } + + $scope.$watch('isOpen', function(value) { + if (value) { + if (!$scope.disabled) { + $timeout(function() { + positionPopup(); + + if (onOpenFocus) { + $scope.$broadcast('uib:datepicker.focus'); + } + + $document.on('click', documentClickBind); + + var placement = $attrs.popupPlacement ? $attrs.popupPlacement : datepickerPopupConfig.placement; + if (appendToBody || $position.parsePlacement(placement)[2]) { + scrollParentEl = scrollParentEl || angular.element($position.scrollParent($element)); + if (scrollParentEl) { + scrollParentEl.on('scroll', positionPopup); + } + } else { + scrollParentEl = null; + } + + angular.element($window).on('resize', positionPopup); + }, 0, false); + } else { + $scope.isOpen = false; + } + } else { + $document.off('click', documentClickBind); + if (scrollParentEl) { + scrollParentEl.off('scroll', positionPopup); + } + angular.element($window).off('resize', positionPopup); + } + }); + + function cameltoDash(string) { + return string.replace(/([A-Z])/g, function($1) { return '-' + $1.toLowerCase(); }); + } + + function parseDateString(viewValue) { + var date = dateParser.parse(viewValue, dateFormat, $scope.date); + if (isNaN(date)) { + for (var i = 0; i < altInputFormats.length; i++) { + date = dateParser.parse(viewValue, altInputFormats[i], $scope.date); + if (!isNaN(date)) { + return date; + } + } + } + return date; + } + + function parseDate(viewValue) { + if (angular.isNumber(viewValue)) { + // presumably timestamp to date object + viewValue = new Date(viewValue); + } + + if (!viewValue) { + return null; + } + + if (angular.isDate(viewValue) && !isNaN(viewValue)) { + return viewValue; + } + + if (angular.isString(viewValue)) { + var date = parseDateString(viewValue); + if (!isNaN(date)) { + return dateParser.toTimezone(date, timezone); + } + } + + return ngModel.$options && ngModel.$options.allowInvalid ? viewValue : undefined; + } + + function validator(modelValue, viewValue) { + var value = modelValue || viewValue; + + if (!$attrs.ngRequired && !value) { + return true; + } + + if (angular.isNumber(value)) { + value = new Date(value); + } + + if (!value) { + return true; + } + + if (angular.isDate(value) && !isNaN(value)) { + return true; + } + + if (angular.isString(value)) { + return !isNaN(parseDateString(viewValue)); + } + + return false; + } + + function documentClickBind(event) { + if (!$scope.isOpen && $scope.disabled) { + return; + } + + var popup = $popup[0]; + var dpContainsTarget = $element[0].contains(event.target); + // The popup node may not be an element node + // In some browsers (IE) only element nodes have the 'contains' function + var popupContainsTarget = popup.contains !== undefined && popup.contains(event.target); + if ($scope.isOpen && !(dpContainsTarget || popupContainsTarget)) { + $scope.$apply(function() { + $scope.isOpen = false; + }); + } + } + + function inputKeydownBind(evt) { + if (evt.which === 27 && $scope.isOpen) { + evt.preventDefault(); + evt.stopPropagation(); + $scope.$apply(function() { + $scope.isOpen = false; + }); + $element[0].focus(); + } else if (evt.which === 40 && !$scope.isOpen) { + evt.preventDefault(); + evt.stopPropagation(); + $scope.$apply(function() { + $scope.isOpen = true; + }); + } + } + + function positionPopup() { + if ($scope.isOpen) { + var dpElement = angular.element($popup[0].querySelector('.uib-datepicker-popup')); + var placement = $attrs.popupPlacement ? $attrs.popupPlacement : datepickerPopupConfig.placement; + var position = $position.positionElements($element, dpElement, placement, appendToBody); + dpElement.css({top: position.top + 'px', left: position.left + 'px'}); + if (dpElement.hasClass('uib-position-measure')) { + dpElement.removeClass('uib-position-measure'); + } + } + } + + $scope.$on('uib:datepicker.mode', function() { + $timeout(positionPopup, 0, false); + }); +}]) + +.directive('uibDatepickerPopup', function() { + return { + require: ['ngModel', 'uibDatepickerPopup'], + controller: 'UibDatepickerPopupController', + scope: { + datepickerOptions: '=?', + isOpen: '=?', + currentText: '@', + clearText: '@', + closeText: '@' + }, + link: function(scope, element, attrs, ctrls) { + var ngModel = ctrls[0], + ctrl = ctrls[1]; + + ctrl.init(ngModel); + } + }; +}) + +.directive('uibDatepickerPopupWrap', function() { + return { + replace: true, + transclude: true, + templateUrl: function(element, attrs) { + return attrs.templateUrl || 'uib/template/datepickerPopup/popup.html'; + } + }; +}); + +angular.module('ui.bootstrap.debounce', []) +/** + * A helper, internal service that debounces a function + */ + .factory('$$debounce', ['$timeout', function($timeout) { + return function(callback, debounceTime) { + var timeoutPromise; + + return function() { + var self = this; + var args = Array.prototype.slice.call(arguments); + if (timeoutPromise) { + $timeout.cancel(timeoutPromise); + } + + timeoutPromise = $timeout(function() { + callback.apply(self, args); + }, debounceTime); + }; + }; + }]); + +angular.module('ui.bootstrap.dropdown', ['ui.bootstrap.position']) + +.constant('uibDropdownConfig', { + appendToOpenClass: 'uib-dropdown-open', openClass: 'open' }) -.service('dropdownService', ['$document', function($document) { +.service('uibDropdownService', ['$document', '$rootScope', function($document, $rootScope) { var openScope = null; - this.open = function( dropdownScope ) { - if ( !openScope ) { - $document.bind('click', closeDropdown); - $document.bind('keydown', escapeKeyBind); + this.open = function(dropdownScope, element) { + if (!openScope) { + $document.on('click', closeDropdown); + element.on('keydown', keybindFilter); } - if ( openScope && openScope !== dropdownScope ) { - openScope.isOpen = false; + if (openScope && openScope !== dropdownScope) { + openScope.isOpen = false; } openScope = dropdownScope; }; - this.close = function( dropdownScope ) { - if ( openScope === dropdownScope ) { + this.close = function(dropdownScope, element) { + if (openScope === dropdownScope) { openScope = null; - $document.unbind('click', closeDropdown); - $document.unbind('keydown', escapeKeyBind); + $document.off('click', closeDropdown); + element.off('keydown', keybindFilter); } }; - var closeDropdown = function( evt ) { - if (evt && evt.isDefaultPrevented()) { - return; + var closeDropdown = function(evt) { + // This method may still be called during the same mouse event that + // unbound this event handler. So check openScope before proceeding. + if (!openScope) { return; } + + if (evt && openScope.getAutoClose() === 'disabled') { return; } + + if (evt && evt.which === 3) { return; } + + var toggleElement = openScope.getToggleElement(); + if (evt && toggleElement && toggleElement[0].contains(evt.target)) { + return; } - openScope.$apply(function() { - openScope.isOpen = false; - }); + var dropdownElement = openScope.getDropdownElement(); + if (evt && openScope.getAutoClose() === 'outsideClick' && + dropdownElement && dropdownElement[0].contains(evt.target)) { + return; + } + + openScope.isOpen = false; + + if (!$rootScope.$$phase) { + openScope.$apply(); + } }; - var escapeKeyBind = function( evt ) { - if ( evt.which === 27 ) { + var keybindFilter = function(evt) { + if (evt.which === 27) { + evt.stopPropagation(); openScope.focusToggleElement(); closeDropdown(); + } else if (openScope.isKeynavEnabled() && [38, 40].indexOf(evt.which) !== -1 && openScope.isOpen) { + evt.preventDefault(); + evt.stopPropagation(); + openScope.focusDropdownEntry(evt.which); } }; }]) -.controller('DropdownController', ['$scope', '$attrs', '$parse', 'dropdownConfig', 'dropdownService', '$animate', function($scope, $attrs, $parse, dropdownConfig, dropdownService, $animate) { +.controller('UibDropdownController', ['$scope', '$element', '$attrs', '$parse', 'uibDropdownConfig', 'uibDropdownService', '$animate', '$uibPosition', '$document', '$compile', '$templateRequest', function($scope, $element, $attrs, $parse, dropdownConfig, uibDropdownService, $animate, $position, $document, $compile, $templateRequest) { var self = this, - scope = $scope.$new(), // create a child scope so we are not polluting original one - openClass = dropdownConfig.openClass, - getIsOpen, - setIsOpen = angular.noop, - toggleInvoker = $attrs.onToggle ? $parse($attrs.onToggle) : angular.noop; + scope = $scope.$new(), // create a child scope so we are not polluting original one + templateScope, + appendToOpenClass = dropdownConfig.appendToOpenClass, + openClass = dropdownConfig.openClass, + getIsOpen, + setIsOpen = angular.noop, + toggleInvoker = $attrs.onToggle ? $parse($attrs.onToggle) : angular.noop, + appendToBody = false, + appendTo = null, + keynavEnabled = false, + selectedOption = null, + body = $document.find('body'); - this.init = function( element ) { - self.$element = element; + $element.addClass('dropdown'); - if ( $attrs.isOpen ) { + this.init = function() { + if ($attrs.isOpen) { getIsOpen = $parse($attrs.isOpen); setIsOpen = getIsOpen.assign; @@ -1668,10 +3121,36 @@ angular.module('ui.bootstrap.dropdown', []) scope.isOpen = !!value; }); } + + if (angular.isDefined($attrs.dropdownAppendTo)) { + var appendToEl = $parse($attrs.dropdownAppendTo)(scope); + if (appendToEl) { + appendTo = angular.element(appendToEl); + } + } + + appendToBody = angular.isDefined($attrs.dropdownAppendToBody); + keynavEnabled = angular.isDefined($attrs.keyboardNav); + + if (appendToBody && !appendTo) { + appendTo = body; + } + + if (appendTo && self.dropdownMenu) { + appendTo.append(self.dropdownMenu); + $element.on('$destroy', function handleDestroyEvent() { + self.dropdownMenu.remove(); + }); + } }; - this.toggle = function( open ) { - return scope.isOpen = arguments.length ? !!open : !scope.isOpen; + this.toggle = function(open) { + scope.isOpen = arguments.length ? !!open : !scope.isOpen; + if (angular.isFunction(setIsOpen)) { + setIsOpen(scope, scope.isOpen); + } + + return scope.isOpen; }; // Allow other directives to watch status @@ -1679,62 +3158,193 @@ angular.module('ui.bootstrap.dropdown', []) return scope.isOpen; }; + scope.getToggleElement = function() { + return self.toggleElement; + }; + + scope.getAutoClose = function() { + return $attrs.autoClose || 'always'; //or 'outsideClick' or 'disabled' + }; + + scope.getElement = function() { + return $element; + }; + + scope.isKeynavEnabled = function() { + return keynavEnabled; + }; + + scope.focusDropdownEntry = function(keyCode) { + var elems = self.dropdownMenu ? //If append to body is used. + angular.element(self.dropdownMenu).find('a') : + $element.find('ul').eq(0).find('a'); + + switch (keyCode) { + case 40: { + if (!angular.isNumber(self.selectedOption)) { + self.selectedOption = 0; + } else { + self.selectedOption = self.selectedOption === elems.length - 1 ? + self.selectedOption : + self.selectedOption + 1; + } + break; + } + case 38: { + if (!angular.isNumber(self.selectedOption)) { + self.selectedOption = elems.length - 1; + } else { + self.selectedOption = self.selectedOption === 0 ? + 0 : self.selectedOption - 1; + } + break; + } + } + elems[self.selectedOption].focus(); + }; + + scope.getDropdownElement = function() { + return self.dropdownMenu; + }; + scope.focusToggleElement = function() { - if ( self.toggleElement ) { + if (self.toggleElement) { self.toggleElement[0].focus(); } }; - scope.$watch('isOpen', function( isOpen, wasOpen ) { - $animate[isOpen ? 'addClass' : 'removeClass'](self.$element, openClass); + scope.$watch('isOpen', function(isOpen, wasOpen) { + if (appendTo && self.dropdownMenu) { + var pos = $position.positionElements($element, self.dropdownMenu, 'bottom-left', true), + css, + rightalign; + + css = { + top: pos.top + 'px', + display: isOpen ? 'block' : 'none' + }; + + rightalign = self.dropdownMenu.hasClass('dropdown-menu-right'); + if (!rightalign) { + css.left = pos.left + 'px'; + css.right = 'auto'; + } else { + css.left = 'auto'; + css.right = window.innerWidth - + (pos.left + $element.prop('offsetWidth')) + 'px'; + } + + // Need to adjust our positioning to be relative to the appendTo container + // if it's not the body element + if (!appendToBody) { + var appendOffset = $position.offset(appendTo); + + css.top = pos.top - appendOffset.top + 'px'; + + if (!rightalign) { + css.left = pos.left - appendOffset.left + 'px'; + } else { + css.right = window.innerWidth - + (pos.left - appendOffset.left + $element.prop('offsetWidth')) + 'px'; + } + } + + self.dropdownMenu.css(css); + } + + var openContainer = appendTo ? appendTo : $element; + var hasOpenClass = openContainer.hasClass(appendTo ? appendToOpenClass : openClass); + + if (hasOpenClass === !isOpen) { + $animate[isOpen ? 'addClass' : 'removeClass'](openContainer, appendTo ? appendToOpenClass : openClass).then(function() { + if (angular.isDefined(isOpen) && isOpen !== wasOpen) { + toggleInvoker($scope, { open: !!isOpen }); + } + }); + } + + if (isOpen) { + if (self.dropdownMenuTemplateUrl) { + $templateRequest(self.dropdownMenuTemplateUrl).then(function(tplContent) { + templateScope = scope.$new(); + $compile(tplContent.trim())(templateScope, function(dropdownElement) { + var newEl = dropdownElement; + self.dropdownMenu.replaceWith(newEl); + self.dropdownMenu = newEl; + }); + }); + } - if ( isOpen ) { scope.focusToggleElement(); - dropdownService.open( scope ); + uibDropdownService.open(scope, $element); } else { - dropdownService.close( scope ); + if (self.dropdownMenuTemplateUrl) { + if (templateScope) { + templateScope.$destroy(); + } + var newEl = angular.element(''); + self.dropdownMenu.replaceWith(newEl); + self.dropdownMenu = newEl; + } + + uibDropdownService.close(scope, $element); + self.selectedOption = null; } - setIsOpen($scope, isOpen); - if (angular.isDefined(isOpen) && isOpen !== wasOpen) { - toggleInvoker($scope, { open: !!isOpen }); + if (angular.isFunction(setIsOpen)) { + setIsOpen($scope, isOpen); } }); - - $scope.$on('$locationChangeSuccess', function() { - scope.isOpen = false; - }); - - $scope.$on('$destroy', function() { - scope.$destroy(); - }); }]) -.directive('dropdown', function() { +.directive('uibDropdown', function() { return { - restrict: 'CA', - controller: 'DropdownController', + controller: 'UibDropdownController', link: function(scope, element, attrs, dropdownCtrl) { - dropdownCtrl.init( element ); + dropdownCtrl.init(); } }; }) -.directive('dropdownToggle', function() { +.directive('uibDropdownMenu', function() { return { - restrict: 'CA', - require: '?^dropdown', + restrict: 'A', + require: '?^uibDropdown', link: function(scope, element, attrs, dropdownCtrl) { - if ( !dropdownCtrl ) { + if (!dropdownCtrl || angular.isDefined(attrs.dropdownNested)) { return; } + element.addClass('dropdown-menu'); + + var tplUrl = attrs.templateUrl; + if (tplUrl) { + dropdownCtrl.dropdownMenuTemplateUrl = tplUrl; + } + + if (!dropdownCtrl.dropdownMenu) { + dropdownCtrl.dropdownMenu = element; + } + } + }; +}) + +.directive('uibDropdownToggle', function() { + return { + require: '?^uibDropdown', + link: function(scope, element, attrs, dropdownCtrl) { + if (!dropdownCtrl) { + return; + } + + element.addClass('dropdown-toggle'); + dropdownCtrl.toggleElement = element; var toggleDropdown = function(event) { event.preventDefault(); - if ( !element.hasClass('disabled') && !attrs.disabled ) { + if (!element.hasClass('disabled') && !attrs.disabled) { scope.$apply(function() { dropdownCtrl.toggle(); }); @@ -1745,7 +3355,7 @@ angular.module('ui.bootstrap.dropdown', []) // WAI-ARIA element.attr({ 'aria-haspopup': true, 'aria-expanded': false }); - scope.$watch(dropdownCtrl.isOpen, function( isOpen ) { + scope.$watch(dropdownCtrl.isOpen, function(isOpen) { element.attr('aria-expanded', !!isOpen); }); @@ -1756,27 +3366,26 @@ angular.module('ui.bootstrap.dropdown', []) }; }); -angular.module('ui.bootstrap.modal', ['ui.bootstrap.transition']) - +angular.module('ui.bootstrap.stackedMap', []) /** * A helper, internal data structure that acts as a map but also allows getting / removing * elements in the LIFO order */ - .factory('$$stackedMap', function () { + .factory('$$stackedMap', function() { return { - createNew: function () { + createNew: function() { var stack = []; return { - add: function (key, value) { + add: function(key, value) { stack.push({ key: key, value: value }); }, - get: function (key) { + get: function(key) { for (var i = 0; i < stack.length; i++) { - if (key == stack[i].key) { + if (key === stack[i].key) { return stack[i]; } } @@ -1788,93 +3397,302 @@ angular.module('ui.bootstrap.modal', ['ui.bootstrap.transition']) } return keys; }, - top: function () { + top: function() { return stack[stack.length - 1]; }, - remove: function (key) { + remove: function(key) { var idx = -1; for (var i = 0; i < stack.length; i++) { - if (key == stack[i].key) { + if (key === stack[i].key) { idx = i; break; } } return stack.splice(idx, 1)[0]; }, - removeTop: function () { + removeTop: function() { return stack.splice(stack.length - 1, 1)[0]; }, - length: function () { + length: function() { return stack.length; } }; } }; + }); +angular.module('ui.bootstrap.modal', ['ui.bootstrap.stackedMap', 'ui.bootstrap.position']) +/** + * A helper, internal data structure that stores all references attached to key + */ + .factory('$$multiMap', function() { + return { + createNew: function() { + var map = {}; + + return { + entries: function() { + return Object.keys(map).map(function(key) { + return { + key: key, + value: map[key] + }; + }); + }, + get: function(key) { + return map[key]; + }, + hasKey: function(key) { + return !!map[key]; + }, + keys: function() { + return Object.keys(map); + }, + put: function(key, value) { + if (!map[key]) { + map[key] = []; + } + + map[key].push(value); + }, + remove: function(key, value) { + var values = map[key]; + + if (!values) { + return; + } + + var idx = values.indexOf(value); + + if (idx !== -1) { + values.splice(idx, 1); + } + + if (!values.length) { + delete map[key]; + } + } + }; + } + }; + }) + +/** + * Pluggable resolve mechanism for the modal resolve resolution + * Supports UI Router's $resolve service + */ + .provider('$uibResolve', function() { + var resolve = this; + this.resolver = null; + + this.setResolver = function(resolver) { + this.resolver = resolver; + }; + + this.$get = ['$injector', '$q', function($injector, $q) { + var resolver = resolve.resolver ? $injector.get(resolve.resolver) : null; + return { + resolve: function(invocables, locals, parent, self) { + if (resolver) { + return resolver.resolve(invocables, locals, parent, self); + } + + var promises = []; + + angular.forEach(invocables, function(value) { + if (angular.isFunction(value) || angular.isArray(value)) { + promises.push($q.resolve($injector.invoke(value))); + } else if (angular.isString(value)) { + promises.push($q.resolve($injector.get(value))); + } else { + promises.push($q.resolve(value)); + } + }); + + return $q.all(promises).then(function(resolves) { + var resolveObj = {}; + var resolveIter = 0; + angular.forEach(invocables, function(value, key) { + resolveObj[key] = resolves[resolveIter++]; + }); + + return resolveObj; + }); + } + }; + }]; }) /** * A helper directive for the $modal service. It creates a backdrop element. */ - .directive('modalBackdrop', ['$timeout', function ($timeout) { + .directive('uibModalBackdrop', ['$animate', '$injector', '$uibModalStack', + function($animate, $injector, $modalStack) { return { - restrict: 'EA', replace: true, - templateUrl: 'template/modal/backdrop.html', - link: function (scope) { - - scope.animate = false; - - //trigger CSS transitions - $timeout(function () { - scope.animate = true; - }); + templateUrl: 'uib/template/modal/backdrop.html', + compile: function(tElement, tAttrs) { + tElement.addClass(tAttrs.backdropClass); + return linkFn; } }; + + function linkFn(scope, element, attrs) { + if (attrs.modalInClass) { + $animate.addClass(element, attrs.modalInClass); + + scope.$on($modalStack.NOW_CLOSING_EVENT, function(e, setIsAsync) { + var done = setIsAsync(); + if (scope.modalOptions.animation) { + $animate.removeClass(element, attrs.modalInClass).then(done); + } else { + done(); + } + }); + } + } }]) - .directive('modalWindow', ['$modalStack', '$timeout', function ($modalStack, $timeout) { + .directive('uibModalWindow', ['$uibModalStack', '$q', '$animateCss', '$document', + function($modalStack, $q, $animateCss, $document) { return { - restrict: 'EA', scope: { - index: '@', - animate: '=' + index: '@' }, replace: true, transclude: true, templateUrl: function(tElement, tAttrs) { - return tAttrs.templateUrl || 'template/modal/window.html'; + return tAttrs.templateUrl || 'uib/template/modal/window.html'; }, - link: function (scope, element, attrs) { + link: function(scope, element, attrs) { element.addClass(attrs.windowClass || ''); + element.addClass(attrs.windowTopClass || ''); scope.size = attrs.size; - $timeout(function () { - // trigger CSS transitions - scope.animate = true; - // focus a freshly-opened modal - element[0].focus(); - }); - - scope.close = function (evt) { + scope.close = function(evt) { var modal = $modalStack.getTop(); - if (modal && modal.value.backdrop && modal.value.backdrop != 'static' && (evt.target === evt.currentTarget)) { + if (modal && modal.value.backdrop && + modal.value.backdrop !== 'static' && + evt.target === evt.currentTarget) { evt.preventDefault(); evt.stopPropagation(); $modalStack.dismiss(modal.key, 'backdrop click'); } }; + + // moved from template to fix issue #2280 + element.on('click', scope.close); + + // This property is only added to the scope for the purpose of detecting when this directive is rendered. + // We can detect that by using this property in the template associated with this directive and then use + // {@link Attribute#$observe} on it. For more details please see {@link TableColumnResize}. + scope.$isRendered = true; + + // Deferred object that will be resolved when this modal is render. + var modalRenderDeferObj = $q.defer(); + // Observe function will be called on next digest cycle after compilation, ensuring that the DOM is ready. + // In order to use this way of finding whether DOM is ready, we need to observe a scope property used in modal's template. + attrs.$observe('modalRender', function(value) { + if (value === 'true') { + modalRenderDeferObj.resolve(); + } + }); + + modalRenderDeferObj.promise.then(function() { + var animationPromise = null; + + if (attrs.modalInClass) { + animationPromise = $animateCss(element, { + addClass: attrs.modalInClass + }).start(); + + scope.$on($modalStack.NOW_CLOSING_EVENT, function(e, setIsAsync) { + var done = setIsAsync(); + $animateCss(element, { + removeClass: attrs.modalInClass + }).start().then(done); + }); + } + + + $q.when(animationPromise).then(function() { + // Notify {@link $modalStack} that modal is rendered. + var modal = $modalStack.getTop(); + if (modal) { + $modalStack.modalRendered(modal.key); + } + + /** + * If something within the freshly-opened modal already has focus (perhaps via a + * directive that causes focus). then no need to try and focus anything. + */ + if (!($document[0].activeElement && element[0].contains($document[0].activeElement))) { + var inputWithAutofocus = element[0].querySelector('[autofocus]'); + /** + * Auto-focusing of a freshly-opened modal element causes any child elements + * with the autofocus attribute to lose focus. This is an issue on touch + * based devices which will show and then hide the onscreen keyboard. + * Attempts to refocus the autofocus element via JavaScript will not reopen + * the onscreen keyboard. Fixed by updated the focusing logic to only autofocus + * the modal element if the modal does not contain an autofocus element. + */ + if (inputWithAutofocus) { + inputWithAutofocus.focus(); + } else { + element[0].focus(); + } + } + }); + }); } }; }]) - .factory('$modalStack', ['$transition', '$timeout', '$document', '$compile', '$rootScope', '$$stackedMap', - function ($transition, $timeout, $document, $compile, $rootScope, $$stackedMap) { + .directive('uibModalAnimationClass', function() { + return { + compile: function(tElement, tAttrs) { + if (tAttrs.modalAnimation) { + tElement.addClass(tAttrs.uibModalAnimationClass); + } + } + }; + }) + .directive('uibModalTransclude', function() { + return { + link: function(scope, element, attrs, controller, transclude) { + transclude(scope.$parent, function(clone) { + element.empty(); + element.append(clone); + }); + } + }; + }) + + .factory('$uibModalStack', ['$animate', '$animateCss', '$document', + '$compile', '$rootScope', '$q', '$$multiMap', '$$stackedMap', '$uibPosition', + function($animate, $animateCss, $document, $compile, $rootScope, $q, $$multiMap, $$stackedMap, $uibPosition) { var OPENED_MODAL_CLASS = 'modal-open'; var backdropDomEl, backdropScope; var openedWindows = $$stackedMap.createNew(); - var $modalStack = {}; + var openedClasses = $$multiMap.createNew(); + var $modalStack = { + NOW_CLOSING_EVENT: 'modal.stack.now-closing' + }; + var topModalIndex = 0; + var previousTopOpenedModal = null; + + //Modal focus behavior + var tabableSelector = 'a[href], area[href], input:not([disabled]), ' + + 'button:not([disabled]),select:not([disabled]), textarea:not([disabled]), ' + + 'iframe, object, embed, *[tabindex], *[contenteditable=true]'; + var scrollbarPadding; + + function isVisible(element) { + return !!(element.offsetWidth || + element.offsetHeight || + element.getClientRects().length); + } function backdropIndex() { var topBackdropIndex = -1; @@ -1884,62 +3702,98 @@ angular.module('ui.bootstrap.modal', ['ui.bootstrap.transition']) topBackdropIndex = i; } } + + // If any backdrop exist, ensure that it's index is always + // right below the top modal + if (topBackdropIndex > -1 && topBackdropIndex < topModalIndex) { + topBackdropIndex = topModalIndex; + } return topBackdropIndex; } - $rootScope.$watch(backdropIndex, function(newBackdropIndex){ + $rootScope.$watch(backdropIndex, function(newBackdropIndex) { if (backdropScope) { backdropScope.index = newBackdropIndex; } }); - function removeModalWindow(modalInstance) { - - var body = $document.find('body').eq(0); + function removeModalWindow(modalInstance, elementToReceiveFocus) { var modalWindow = openedWindows.get(modalInstance).value; + var appendToElement = modalWindow.appendTo; //clean up the stack openedWindows.remove(modalInstance); + previousTopOpenedModal = openedWindows.top(); + if (previousTopOpenedModal) { + topModalIndex = parseInt(previousTopOpenedModal.value.modalDomEl.attr('index'), 10); + } - //remove window DOM element - removeAfterAnimate(modalWindow.modalDomEl, modalWindow.modalScope, 300, function() { - modalWindow.modalScope.$destroy(); - body.toggleClass(OPENED_MODAL_CLASS, openedWindows.length() > 0); - checkRemoveBackdrop(); - }); + removeAfterAnimate(modalWindow.modalDomEl, modalWindow.modalScope, function() { + var modalBodyClass = modalWindow.openedClass || OPENED_MODAL_CLASS; + openedClasses.remove(modalBodyClass, modalInstance); + var areAnyOpen = openedClasses.hasKey(modalBodyClass); + appendToElement.toggleClass(modalBodyClass, areAnyOpen); + if (!areAnyOpen && scrollbarPadding && scrollbarPadding.heightOverflow && scrollbarPadding.scrollbarWidth) { + if (scrollbarPadding.originalRight) { + appendToElement.css({paddingRight: scrollbarPadding.originalRight + 'px'}); + } else { + appendToElement.css({paddingRight: ''}); + } + scrollbarPadding = null; + } + toggleTopWindowClass(true); + }, modalWindow.closedDeferred); + checkRemoveBackdrop(); + + //move focus to specified element if available, or else to body + if (elementToReceiveFocus && elementToReceiveFocus.focus) { + elementToReceiveFocus.focus(); + } else if (appendToElement.focus) { + appendToElement.focus(); + } + } + + // Add or remove "windowTopClass" from the top window in the stack + function toggleTopWindowClass(toggleSwitch) { + var modalWindow; + + if (openedWindows.length() > 0) { + modalWindow = openedWindows.top().value; + modalWindow.modalDomEl.toggleClass(modalWindow.windowTopClass || '', toggleSwitch); + } } function checkRemoveBackdrop() { - //remove backdrop if no longer needed - if (backdropDomEl && backdropIndex() == -1) { - var backdropScopeRef = backdropScope; - removeAfterAnimate(backdropDomEl, backdropScope, 150, function () { - backdropScopeRef.$destroy(); - backdropScopeRef = null; - }); - backdropDomEl = undefined; - backdropScope = undefined; - } + //remove backdrop if no longer needed + if (backdropDomEl && backdropIndex() === -1) { + var backdropScopeRef = backdropScope; + removeAfterAnimate(backdropDomEl, backdropScope, function() { + backdropScopeRef = null; + }); + backdropDomEl = undefined; + backdropScope = undefined; + } } - function removeAfterAnimate(domEl, scope, emulateTime, done) { - // Closing animation - scope.animate = false; + function removeAfterAnimate(domEl, scope, done, closedDeferred) { + var asyncDeferred; + var asyncPromise = null; + var setIsAsync = function() { + if (!asyncDeferred) { + asyncDeferred = $q.defer(); + asyncPromise = asyncDeferred.promise; + } - var transitionEndEventName = $transition.transitionEndEventName; - if (transitionEndEventName) { - // transition out - var timeout = $timeout(afterAnimating, emulateTime); + return function asyncDone() { + asyncDeferred.resolve(); + }; + }; + scope.$broadcast($modalStack.NOW_CLOSING_EVENT, setIsAsync); - domEl.bind(transitionEndEventName, function () { - $timeout.cancel(timeout); - afterAnimating(); - scope.$apply(); - }); - } else { - // Ensure this call is async - $timeout(afterAnimating, 0); - } + // Note that it's intentional that asyncPromise might be null. + // That's when setIsAsync has not been called during the + // NOW_CLOSING_EVENT broadcast. + return $q.when(asyncPromise).then(afterAnimating); function afterAnimating() { if (afterAnimating.done) { @@ -1947,141 +3801,284 @@ angular.module('ui.bootstrap.modal', ['ui.bootstrap.transition']) } afterAnimating.done = true; - domEl.remove(); + $animate.leave(domEl).then(function() { + domEl.remove(); + if (closedDeferred) { + closedDeferred.resolve(); + } + }); + + scope.$destroy(); if (done) { done(); } } } - $document.bind('keydown', function (evt) { - var modal; + $document.on('keydown', keydownListener); - if (evt.which === 27) { - modal = openedWindows.top(); - if (modal && modal.value.keyboard) { - evt.preventDefault(); - $rootScope.$apply(function () { - $modalStack.dismiss(modal.key, 'escape key press'); - }); - } - } + $rootScope.$on('$destroy', function() { + $document.off('keydown', keydownListener); }); - $modalStack.open = function (modalInstance, modal) { + function keydownListener(evt) { + if (evt.isDefaultPrevented()) { + return evt; + } + + var modal = openedWindows.top(); + if (modal) { + switch (evt.which) { + case 27: { + if (modal.value.keyboard) { + evt.preventDefault(); + $rootScope.$apply(function() { + $modalStack.dismiss(modal.key, 'escape key press'); + }); + } + break; + } + case 9: { + var list = $modalStack.loadFocusElementList(modal); + var focusChanged = false; + if (evt.shiftKey) { + if ($modalStack.isFocusInFirstItem(evt, list) || $modalStack.isModalFocused(evt, modal)) { + focusChanged = $modalStack.focusLastFocusableElement(list); + } + } else { + if ($modalStack.isFocusInLastItem(evt, list)) { + focusChanged = $modalStack.focusFirstFocusableElement(list); + } + } + + if (focusChanged) { + evt.preventDefault(); + evt.stopPropagation(); + } + + break; + } + } + } + } + + $modalStack.open = function(modalInstance, modal) { + var modalOpener = $document[0].activeElement, + modalBodyClass = modal.openedClass || OPENED_MODAL_CLASS; + + toggleTopWindowClass(false); + + // Store the current top first, to determine what index we ought to use + // for the current top modal + previousTopOpenedModal = openedWindows.top(); openedWindows.add(modalInstance, { deferred: modal.deferred, + renderDeferred: modal.renderDeferred, + closedDeferred: modal.closedDeferred, modalScope: modal.scope, backdrop: modal.backdrop, - keyboard: modal.keyboard + keyboard: modal.keyboard, + openedClass: modal.openedClass, + windowTopClass: modal.windowTopClass, + animation: modal.animation, + appendTo: modal.appendTo }); - var body = $document.find('body').eq(0), + openedClasses.put(modalBodyClass, modalInstance); + + var appendToElement = modal.appendTo, currBackdropIndex = backdropIndex(); + if (!appendToElement.length) { + throw new Error('appendTo element not found. Make sure that the element passed is in DOM.'); + } + if (currBackdropIndex >= 0 && !backdropDomEl) { backdropScope = $rootScope.$new(true); + backdropScope.modalOptions = modal; backdropScope.index = currBackdropIndex; - backdropDomEl = $compile('
')(backdropScope); - body.append(backdropDomEl); + backdropDomEl = angular.element('
'); + backdropDomEl.attr('backdrop-class', modal.backdropClass); + if (modal.animation) { + backdropDomEl.attr('modal-animation', 'true'); + } + $compile(backdropDomEl)(backdropScope); + $animate.enter(backdropDomEl, appendToElement); + scrollbarPadding = $uibPosition.scrollbarPadding(appendToElement); + if (scrollbarPadding.heightOverflow && scrollbarPadding.scrollbarWidth) { + appendToElement.css({paddingRight: scrollbarPadding.right + 'px'}); + } } - var angularDomEl = angular.element('
'); + // Set the top modal index based on the index of the previous top modal + topModalIndex = previousTopOpenedModal ? parseInt(previousTopOpenedModal.value.modalDomEl.attr('index'), 10) + 1 : 0; + var angularDomEl = angular.element('
'); angularDomEl.attr({ 'template-url': modal.windowTemplateUrl, 'window-class': modal.windowClass, + 'window-top-class': modal.windowTopClass, 'size': modal.size, - 'index': openedWindows.length() - 1, + 'index': topModalIndex, 'animate': 'animate' }).html(modal.content); - - var modalDomEl = $compile(angularDomEl)(modal.scope); - openedWindows.top().value.modalDomEl = modalDomEl; - body.append(modalDomEl); - body.addClass(OPENED_MODAL_CLASS); - }; - - $modalStack.close = function (modalInstance, result) { - var modalWindow = openedWindows.get(modalInstance).value; - if (modalWindow) { - modalWindow.deferred.resolve(result); - removeModalWindow(modalInstance); + if (modal.animation) { + angularDomEl.attr('modal-animation', 'true'); } + + appendToElement.addClass(modalBodyClass); + $animate.enter($compile(angularDomEl)(modal.scope), appendToElement); + + openedWindows.top().value.modalDomEl = angularDomEl; + openedWindows.top().value.modalOpener = modalOpener; }; - $modalStack.dismiss = function (modalInstance, reason) { - var modalWindow = openedWindows.get(modalInstance).value; - if (modalWindow) { - modalWindow.deferred.reject(reason); - removeModalWindow(modalInstance); + function broadcastClosing(modalWindow, resultOrReason, closing) { + return !modalWindow.value.modalScope.$broadcast('modal.closing', resultOrReason, closing).defaultPrevented; + } + + $modalStack.close = function(modalInstance, result) { + var modalWindow = openedWindows.get(modalInstance); + if (modalWindow && broadcastClosing(modalWindow, result, true)) { + modalWindow.value.modalScope.$$uibDestructionScheduled = true; + modalWindow.value.deferred.resolve(result); + removeModalWindow(modalInstance, modalWindow.value.modalOpener); + return true; } + return !modalWindow; }; - $modalStack.dismissAll = function (reason) { + $modalStack.dismiss = function(modalInstance, reason) { + var modalWindow = openedWindows.get(modalInstance); + if (modalWindow && broadcastClosing(modalWindow, reason, false)) { + modalWindow.value.modalScope.$$uibDestructionScheduled = true; + modalWindow.value.deferred.reject(reason); + removeModalWindow(modalInstance, modalWindow.value.modalOpener); + return true; + } + return !modalWindow; + }; + + $modalStack.dismissAll = function(reason) { var topModal = this.getTop(); - while (topModal) { - this.dismiss(topModal.key, reason); + while (topModal && this.dismiss(topModal.key, reason)) { topModal = this.getTop(); } }; - $modalStack.getTop = function () { + $modalStack.getTop = function() { return openedWindows.top(); }; + $modalStack.modalRendered = function(modalInstance) { + var modalWindow = openedWindows.get(modalInstance); + if (modalWindow) { + modalWindow.value.renderDeferred.resolve(); + } + }; + + $modalStack.focusFirstFocusableElement = function(list) { + if (list.length > 0) { + list[0].focus(); + return true; + } + return false; + }; + + $modalStack.focusLastFocusableElement = function(list) { + if (list.length > 0) { + list[list.length - 1].focus(); + return true; + } + return false; + }; + + $modalStack.isModalFocused = function(evt, modalWindow) { + if (evt && modalWindow) { + var modalDomEl = modalWindow.value.modalDomEl; + if (modalDomEl && modalDomEl.length) { + return (evt.target || evt.srcElement) === modalDomEl[0]; + } + } + return false; + }; + + $modalStack.isFocusInFirstItem = function(evt, list) { + if (list.length > 0) { + return (evt.target || evt.srcElement) === list[0]; + } + return false; + }; + + $modalStack.isFocusInLastItem = function(evt, list) { + if (list.length > 0) { + return (evt.target || evt.srcElement) === list[list.length - 1]; + } + return false; + }; + + $modalStack.loadFocusElementList = function(modalWindow) { + if (modalWindow) { + var modalDomE1 = modalWindow.value.modalDomEl; + if (modalDomE1 && modalDomE1.length) { + var elements = modalDomE1[0].querySelectorAll(tabableSelector); + return elements ? + Array.prototype.filter.call(elements, function(element) { + return isVisible(element); + }) : elements; + } + } + }; + return $modalStack; }]) - .provider('$modal', function () { - + .provider('$uibModal', function() { var $modalProvider = { options: { - backdrop: true, //can be also false or 'static' + animation: true, + backdrop: true, //can also be false or 'static' keyboard: true }, - $get: ['$injector', '$rootScope', '$q', '$http', '$templateCache', '$controller', '$modalStack', - function ($injector, $rootScope, $q, $http, $templateCache, $controller, $modalStack) { - + $get: ['$rootScope', '$q', '$document', '$templateRequest', '$controller', '$uibResolve', '$uibModalStack', + function ($rootScope, $q, $document, $templateRequest, $controller, $uibResolve, $modalStack) { var $modal = {}; function getTemplatePromise(options) { return options.template ? $q.when(options.template) : - $http.get(options.templateUrl, {cache: $templateCache}).then(function (result) { - return result.data; - }); + $templateRequest(angular.isFunction(options.templateUrl) ? + options.templateUrl() : options.templateUrl); } - function getResolvePromises(resolves) { - var promisesArr = []; - angular.forEach(resolves, function (value, key) { - if (angular.isFunction(value) || angular.isArray(value)) { - promisesArr.push($q.when($injector.invoke(value))); - } - }); - return promisesArr; - } - - $modal.open = function (modalOptions) { + var promiseChain = null; + $modal.getPromiseChain = function() { + return promiseChain; + }; + $modal.open = function(modalOptions) { var modalResultDeferred = $q.defer(); var modalOpenedDeferred = $q.defer(); + var modalClosedDeferred = $q.defer(); + var modalRenderDeferred = $q.defer(); //prepare an instance of a modal to be injected into controllers and returned to a caller var modalInstance = { result: modalResultDeferred.promise, opened: modalOpenedDeferred.promise, + closed: modalClosedDeferred.promise, + rendered: modalRenderDeferred.promise, close: function (result) { - $modalStack.close(modalInstance, result); + return $modalStack.close(modalInstance, result); }, dismiss: function (reason) { - $modalStack.dismiss(modalInstance, reason); + return $modalStack.dismiss(modalInstance, reason); } }; //merge and clean up options modalOptions = angular.extend({}, $modalProvider.options, modalOptions); modalOptions.resolve = modalOptions.resolve || {}; + modalOptions.appendTo = modalOptions.appendTo || $document.find('body').eq(0); //verify options if (!modalOptions.template && !modalOptions.templateUrl) { @@ -2089,261 +4086,379 @@ angular.module('ui.bootstrap.modal', ['ui.bootstrap.transition']) } var templateAndResolvePromise = - $q.all([getTemplatePromise(modalOptions)].concat(getResolvePromises(modalOptions.resolve))); + $q.all([getTemplatePromise(modalOptions), $uibResolve.resolve(modalOptions.resolve, {}, null, null)]); + function resolveWithTemplate() { + return templateAndResolvePromise; + } - templateAndResolvePromise.then(function resolveSuccess(tplAndVars) { + // Wait for the resolution of the existing promise chain. + // Then switch to our own combined promise dependency (regardless of how the previous modal fared). + // Then add to $modalStack and resolve opened. + // Finally clean up the chain variable if no subsequent modal has overwritten it. + var samePromise; + samePromise = promiseChain = $q.all([promiseChain]) + .then(resolveWithTemplate, resolveWithTemplate) + .then(function resolveSuccess(tplAndVars) { + var providedScope = modalOptions.scope || $rootScope; - var modalScope = (modalOptions.scope || $rootScope).$new(); - modalScope.$close = modalInstance.close; - modalScope.$dismiss = modalInstance.dismiss; + var modalScope = providedScope.$new(); + modalScope.$close = modalInstance.close; + modalScope.$dismiss = modalInstance.dismiss; - var ctrlInstance, ctrlLocals = {}; - var resolveIter = 1; - - //controllers - if (modalOptions.controller) { - ctrlLocals.$scope = modalScope; - ctrlLocals.$modalInstance = modalInstance; - angular.forEach(modalOptions.resolve, function (value, key) { - ctrlLocals[key] = tplAndVars[resolveIter++]; + modalScope.$on('$destroy', function() { + if (!modalScope.$$uibDestructionScheduled) { + modalScope.$dismiss('$uibUnscheduledDestruction'); + } }); - ctrlInstance = $controller(modalOptions.controller, ctrlLocals); - } + var ctrlInstance, ctrlInstantiate, ctrlLocals = {}; - $modalStack.open(modalInstance, { - scope: modalScope, - deferred: modalResultDeferred, - content: tplAndVars[0], - backdrop: modalOptions.backdrop, - keyboard: modalOptions.keyboard, - windowClass: modalOptions.windowClass, - windowTemplateUrl: modalOptions.windowTemplateUrl, - size: modalOptions.size - }); + //controllers + if (modalOptions.controller) { + ctrlLocals.$scope = modalScope; + ctrlLocals.$uibModalInstance = modalInstance; + angular.forEach(tplAndVars[1], function(value, key) { + ctrlLocals[key] = value; + }); + + // the third param will make the controller instantiate later,private api + // @see https://github.com/angular/angular.js/blob/master/src/ng/controller.js#L126 + ctrlInstantiate = $controller(modalOptions.controller, ctrlLocals, true); + if (modalOptions.controllerAs) { + ctrlInstance = ctrlInstantiate.instance; + + if (modalOptions.bindToController) { + ctrlInstance.$close = modalScope.$close; + ctrlInstance.$dismiss = modalScope.$dismiss; + angular.extend(ctrlInstance, providedScope); + } + + ctrlInstance = ctrlInstantiate(); + + modalScope[modalOptions.controllerAs] = ctrlInstance; + } else { + ctrlInstance = ctrlInstantiate(); + } + + if (angular.isFunction(ctrlInstance.$onInit)) { + ctrlInstance.$onInit(); + } + } + + $modalStack.open(modalInstance, { + scope: modalScope, + deferred: modalResultDeferred, + renderDeferred: modalRenderDeferred, + closedDeferred: modalClosedDeferred, + content: tplAndVars[0], + animation: modalOptions.animation, + backdrop: modalOptions.backdrop, + keyboard: modalOptions.keyboard, + backdropClass: modalOptions.backdropClass, + windowTopClass: modalOptions.windowTopClass, + windowClass: modalOptions.windowClass, + windowTemplateUrl: modalOptions.windowTemplateUrl, + size: modalOptions.size, + openedClass: modalOptions.openedClass, + appendTo: modalOptions.appendTo + }); + modalOpenedDeferred.resolve(true); }, function resolveError(reason) { + modalOpenedDeferred.reject(reason); modalResultDeferred.reject(reason); - }); - - templateAndResolvePromise.then(function () { - modalOpenedDeferred.resolve(true); - }, function () { - modalOpenedDeferred.reject(false); + })['finally'](function() { + if (promiseChain === samePromise) { + promiseChain = null; + } }); return modalInstance; }; return $modal; - }] + } + ] }; return $modalProvider; }); -angular.module('ui.bootstrap.pagination', []) - -.controller('PaginationController', ['$scope', '$attrs', '$parse', function ($scope, $attrs, $parse) { - var self = this, - ngModelCtrl = { $setViewValue: angular.noop }, // nullModelCtrl - setNumPages = $attrs.numPages ? $parse($attrs.numPages).assign : angular.noop; - - this.init = function(ngModelCtrl_, config) { - ngModelCtrl = ngModelCtrl_; - this.config = config; - - ngModelCtrl.$render = function() { - self.render(); - }; - - if ($attrs.itemsPerPage) { - $scope.$parent.$watch($parse($attrs.itemsPerPage), function(value) { - self.itemsPerPage = parseInt(value, 10); - $scope.totalPages = self.calculateTotalPages(); - }); - } else { - this.itemsPerPage = config.itemsPerPage; - } - }; - - this.calculateTotalPages = function() { - var totalPages = this.itemsPerPage < 1 ? 1 : Math.ceil($scope.totalItems / this.itemsPerPage); - return Math.max(totalPages || 0, 1); - }; - - this.render = function() { - $scope.page = parseInt(ngModelCtrl.$viewValue, 10) || 1; - }; - - $scope.selectPage = function(page) { - if ( $scope.page !== page && page > 0 && page <= $scope.totalPages) { - ngModelCtrl.$setViewValue(page); - ngModelCtrl.$render(); - } - }; - - $scope.getText = function( key ) { - return $scope[key + 'Text'] || self.config[key + 'Text']; - }; - $scope.noPrevious = function() { - return $scope.page === 1; - }; - $scope.noNext = function() { - return $scope.page === $scope.totalPages; - }; - - $scope.$watch('totalItems', function() { - $scope.totalPages = self.calculateTotalPages(); - }); - - $scope.$watch('totalPages', function(value) { - setNumPages($scope.$parent, value); // Readonly variable - - if ( $scope.page > value ) { - $scope.selectPage(value); - } else { - ngModelCtrl.$render(); - } - }); -}]) - -.constant('paginationConfig', { - itemsPerPage: 10, - boundaryLinks: false, - directionLinks: true, - firstText: 'First', - previousText: 'Previous', - nextText: 'Next', - lastText: 'Last', - rotate: true -}) - -.directive('pagination', ['$parse', 'paginationConfig', function($parse, paginationConfig) { +angular.module('ui.bootstrap.paging', []) +/** + * Helper internal service for generating common controller code between the + * pager and pagination components + */ +.factory('uibPaging', ['$parse', function($parse) { return { - restrict: 'EA', - scope: { - totalItems: '=', - firstText: '@', - previousText: '@', - nextText: '@', - lastText: '@' - }, - require: ['pagination', '?ngModel'], - controller: 'PaginationController', - templateUrl: 'template/pagination/pagination.html', - replace: true, - link: function(scope, element, attrs, ctrls) { - var paginationCtrl = ctrls[0], ngModelCtrl = ctrls[1]; + create: function(ctrl, $scope, $attrs) { + ctrl.setNumPages = $attrs.numPages ? $parse($attrs.numPages).assign : angular.noop; + ctrl.ngModelCtrl = { $setViewValue: angular.noop }; // nullModelCtrl + ctrl._watchers = []; - if (!ngModelCtrl) { - return; // do nothing if no ng-model - } + ctrl.init = function(ngModelCtrl, config) { + ctrl.ngModelCtrl = ngModelCtrl; + ctrl.config = config; - // Setup configuration parameters - var maxSize = angular.isDefined(attrs.maxSize) ? scope.$parent.$eval(attrs.maxSize) : paginationConfig.maxSize, - rotate = angular.isDefined(attrs.rotate) ? scope.$parent.$eval(attrs.rotate) : paginationConfig.rotate; - scope.boundaryLinks = angular.isDefined(attrs.boundaryLinks) ? scope.$parent.$eval(attrs.boundaryLinks) : paginationConfig.boundaryLinks; - scope.directionLinks = angular.isDefined(attrs.directionLinks) ? scope.$parent.$eval(attrs.directionLinks) : paginationConfig.directionLinks; - - paginationCtrl.init(ngModelCtrl, paginationConfig); - - if (attrs.maxSize) { - scope.$parent.$watch($parse(attrs.maxSize), function(value) { - maxSize = parseInt(value, 10); - paginationCtrl.render(); - }); - } - - // Create page object used in template - function makePage(number, text, isActive) { - return { - number: number, - text: text, - active: isActive + ngModelCtrl.$render = function() { + ctrl.render(); }; - } - function getPages(currentPage, totalPages) { - var pages = []; - - // Default page limits - var startPage = 1, endPage = totalPages; - var isMaxSized = ( angular.isDefined(maxSize) && maxSize < totalPages ); - - // recompute if maxSize - if ( isMaxSized ) { - if ( rotate ) { - // Current page is displayed in the middle of the visible ones - startPage = Math.max(currentPage - Math.floor(maxSize/2), 1); - endPage = startPage + maxSize - 1; - - // Adjust if limit is exceeded - if (endPage > totalPages) { - endPage = totalPages; - startPage = endPage - maxSize + 1; - } - } else { - // Visible pages are paginated with maxSize - startPage = ((Math.ceil(currentPage / maxSize) - 1) * maxSize) + 1; - - // Adjust last page if limit is exceeded - endPage = Math.min(startPage + maxSize - 1, totalPages); - } + if ($attrs.itemsPerPage) { + ctrl._watchers.push($scope.$parent.$watch($attrs.itemsPerPage, function(value) { + ctrl.itemsPerPage = parseInt(value, 10); + $scope.totalPages = ctrl.calculateTotalPages(); + ctrl.updatePage(); + })); + } else { + ctrl.itemsPerPage = config.itemsPerPage; } - // Add page number links - for (var number = startPage; number <= endPage; number++) { - var page = makePage(number, number, number === currentPage); - pages.push(page); + $scope.$watch('totalItems', function(newTotal, oldTotal) { + if (angular.isDefined(newTotal) || newTotal !== oldTotal) { + $scope.totalPages = ctrl.calculateTotalPages(); + ctrl.updatePage(); + } + }); + }; + + ctrl.calculateTotalPages = function() { + var totalPages = ctrl.itemsPerPage < 1 ? 1 : Math.ceil($scope.totalItems / ctrl.itemsPerPage); + return Math.max(totalPages || 0, 1); + }; + + ctrl.render = function() { + $scope.page = parseInt(ctrl.ngModelCtrl.$viewValue, 10) || 1; + }; + + $scope.selectPage = function(page, evt) { + if (evt) { + evt.preventDefault(); } - // Add links to move between page sets - if ( isMaxSized && ! rotate ) { - if ( startPage > 1 ) { - var previousPageSet = makePage(startPage - 1, '...', false); - pages.unshift(previousPageSet); + var clickAllowed = !$scope.ngDisabled || !evt; + if (clickAllowed && $scope.page !== page && page > 0 && page <= $scope.totalPages) { + if (evt && evt.target) { + evt.target.blur(); } - - if ( endPage < totalPages ) { - var nextPageSet = makePage(endPage + 1, '...', false); - pages.push(nextPageSet); - } - } - - return pages; - } - - var originalRender = paginationCtrl.render; - paginationCtrl.render = function() { - originalRender(); - if (scope.page > 0 && scope.page <= scope.totalPages) { - scope.pages = getPages(scope.page, scope.totalPages); + ctrl.ngModelCtrl.$setViewValue(page); + ctrl.ngModelCtrl.$render(); } }; + + $scope.getText = function(key) { + return $scope[key + 'Text'] || ctrl.config[key + 'Text']; + }; + + $scope.noPrevious = function() { + return $scope.page === 1; + }; + + $scope.noNext = function() { + return $scope.page === $scope.totalPages; + }; + + ctrl.updatePage = function() { + ctrl.setNumPages($scope.$parent, $scope.totalPages); // Readonly variable + + if ($scope.page > $scope.totalPages) { + $scope.selectPage($scope.totalPages); + } else { + ctrl.ngModelCtrl.$render(); + } + }; + + $scope.$on('$destroy', function() { + while (ctrl._watchers.length) { + ctrl._watchers.shift()(); + } + }); } }; +}]); + +angular.module('ui.bootstrap.pager', ['ui.bootstrap.paging']) + +.controller('UibPagerController', ['$scope', '$attrs', 'uibPaging', 'uibPagerConfig', function($scope, $attrs, uibPaging, uibPagerConfig) { + $scope.align = angular.isDefined($attrs.align) ? $scope.$parent.$eval($attrs.align) : uibPagerConfig.align; + + uibPaging.create(this, $scope, $attrs); }]) -.constant('pagerConfig', { +.constant('uibPagerConfig', { itemsPerPage: 10, previousText: '« Previous', nextText: 'Next »', align: true }) -.directive('pager', ['pagerConfig', function(pagerConfig) { +.directive('uibPager', ['uibPagerConfig', function(uibPagerConfig) { return { - restrict: 'EA', scope: { totalItems: '=', previousText: '@', - nextText: '@' + nextText: '@', + ngDisabled: '=' + }, + require: ['uibPager', '?ngModel'], + controller: 'UibPagerController', + controllerAs: 'pager', + templateUrl: function(element, attrs) { + return attrs.templateUrl || 'uib/template/pager/pager.html'; + }, + replace: true, + link: function(scope, element, attrs, ctrls) { + var paginationCtrl = ctrls[0], ngModelCtrl = ctrls[1]; + + if (!ngModelCtrl) { + return; // do nothing if no ng-model + } + + paginationCtrl.init(ngModelCtrl, uibPagerConfig); + } + }; +}]); + +angular.module('ui.bootstrap.pagination', ['ui.bootstrap.paging']) +.controller('UibPaginationController', ['$scope', '$attrs', '$parse', 'uibPaging', 'uibPaginationConfig', function($scope, $attrs, $parse, uibPaging, uibPaginationConfig) { + var ctrl = this; + // Setup configuration parameters + var maxSize = angular.isDefined($attrs.maxSize) ? $scope.$parent.$eval($attrs.maxSize) : uibPaginationConfig.maxSize, + rotate = angular.isDefined($attrs.rotate) ? $scope.$parent.$eval($attrs.rotate) : uibPaginationConfig.rotate, + forceEllipses = angular.isDefined($attrs.forceEllipses) ? $scope.$parent.$eval($attrs.forceEllipses) : uibPaginationConfig.forceEllipses, + boundaryLinkNumbers = angular.isDefined($attrs.boundaryLinkNumbers) ? $scope.$parent.$eval($attrs.boundaryLinkNumbers) : uibPaginationConfig.boundaryLinkNumbers, + pageLabel = angular.isDefined($attrs.pageLabel) ? function(idx) { return $scope.$parent.$eval($attrs.pageLabel, {$page: idx}); } : angular.identity; + $scope.boundaryLinks = angular.isDefined($attrs.boundaryLinks) ? $scope.$parent.$eval($attrs.boundaryLinks) : uibPaginationConfig.boundaryLinks; + $scope.directionLinks = angular.isDefined($attrs.directionLinks) ? $scope.$parent.$eval($attrs.directionLinks) : uibPaginationConfig.directionLinks; + + uibPaging.create(this, $scope, $attrs); + + if ($attrs.maxSize) { + ctrl._watchers.push($scope.$parent.$watch($parse($attrs.maxSize), function(value) { + maxSize = parseInt(value, 10); + ctrl.render(); + })); + } + + // Create page object used in template + function makePage(number, text, isActive) { + return { + number: number, + text: text, + active: isActive + }; + } + + function getPages(currentPage, totalPages) { + var pages = []; + + // Default page limits + var startPage = 1, endPage = totalPages; + var isMaxSized = angular.isDefined(maxSize) && maxSize < totalPages; + + // recompute if maxSize + if (isMaxSized) { + if (rotate) { + // Current page is displayed in the middle of the visible ones + startPage = Math.max(currentPage - Math.floor(maxSize / 2), 1); + endPage = startPage + maxSize - 1; + + // Adjust if limit is exceeded + if (endPage > totalPages) { + endPage = totalPages; + startPage = endPage - maxSize + 1; + } + } else { + // Visible pages are paginated with maxSize + startPage = (Math.ceil(currentPage / maxSize) - 1) * maxSize + 1; + + // Adjust last page if limit is exceeded + endPage = Math.min(startPage + maxSize - 1, totalPages); + } + } + + // Add page number links + for (var number = startPage; number <= endPage; number++) { + var page = makePage(number, pageLabel(number), number === currentPage); + pages.push(page); + } + + // Add links to move between page sets + if (isMaxSized && maxSize > 0 && (!rotate || forceEllipses || boundaryLinkNumbers)) { + if (startPage > 1) { + if (!boundaryLinkNumbers || startPage > 3) { //need ellipsis for all options unless range is too close to beginning + var previousPageSet = makePage(startPage - 1, '...', false); + pages.unshift(previousPageSet); + } + if (boundaryLinkNumbers) { + if (startPage === 3) { //need to replace ellipsis when the buttons would be sequential + var secondPageLink = makePage(2, '2', false); + pages.unshift(secondPageLink); + } + //add the first page + var firstPageLink = makePage(1, '1', false); + pages.unshift(firstPageLink); + } + } + + if (endPage < totalPages) { + if (!boundaryLinkNumbers || endPage < totalPages - 2) { //need ellipsis for all options unless range is too close to end + var nextPageSet = makePage(endPage + 1, '...', false); + pages.push(nextPageSet); + } + if (boundaryLinkNumbers) { + if (endPage === totalPages - 2) { //need to replace ellipsis when the buttons would be sequential + var secondToLastPageLink = makePage(totalPages - 1, totalPages - 1, false); + pages.push(secondToLastPageLink); + } + //add the last page + var lastPageLink = makePage(totalPages, totalPages, false); + pages.push(lastPageLink); + } + } + } + return pages; + } + + var originalRender = this.render; + this.render = function() { + originalRender(); + if ($scope.page > 0 && $scope.page <= $scope.totalPages) { + $scope.pages = getPages($scope.page, $scope.totalPages); + } + }; +}]) + +.constant('uibPaginationConfig', { + itemsPerPage: 10, + boundaryLinks: false, + boundaryLinkNumbers: false, + directionLinks: true, + firstText: 'First', + previousText: 'Previous', + nextText: 'Next', + lastText: 'Last', + rotate: true, + forceEllipses: false +}) + +.directive('uibPagination', ['$parse', 'uibPaginationConfig', function($parse, uibPaginationConfig) { + return { + scope: { + totalItems: '=', + firstText: '@', + previousText: '@', + nextText: '@', + lastText: '@', + ngDisabled:'=' + }, + require: ['uibPagination', '?ngModel'], + controller: 'UibPaginationController', + controllerAs: 'pagination', + templateUrl: function(element, attrs) { + return attrs.templateUrl || 'uib/template/pagination/pagination.html'; }, - require: ['pager', '?ngModel'], - controller: 'PaginationController', - templateUrl: 'template/pagination/pager.html', replace: true, link: function(scope, element, attrs, ctrls) { var paginationCtrl = ctrls[0], ngModelCtrl = ctrls[1]; @@ -2352,8 +4467,7 @@ angular.module('ui.bootstrap.pagination', []) return; // do nothing if no ng-model } - scope.align = angular.isDefined(attrs.align) ? scope.$parent.$eval(attrs.align) : pagerConfig.align; - paginationCtrl.init(ngModelCtrl, pagerConfig); + paginationCtrl.init(ngModelCtrl, uibPaginationConfig); } }; }]); @@ -2363,25 +4477,30 @@ angular.module('ui.bootstrap.pagination', []) * function, placement as a function, inside, support for more triggers than * just mouse enter/leave, html tooltips, and selector delegation. */ -angular.module( 'ui.bootstrap.tooltip', [ 'ui.bootstrap.position', 'ui.bootstrap.bindHtml' ] ) +angular.module('ui.bootstrap.tooltip', ['ui.bootstrap.position', 'ui.bootstrap.stackedMap']) /** * The $tooltip service creates tooltip- and popover-like directives as well as * houses global options for them. */ -.provider( '$tooltip', function () { +.provider('$uibTooltip', function() { // The default options tooltip and popover. var defaultOptions = { placement: 'top', + placementClassPrefix: '', animation: true, - popupDelay: 0 + popupDelay: 0, + popupCloseDelay: 0, + useContentExp: false }; // Default hide triggers for each show trigger var triggerMap = { 'mouseenter': 'mouseleave', 'click': 'click', - 'focus': 'blur' + 'outsideClick': 'outsideClick', + 'focus': 'blur', + 'none': '' }; // The options specified to the provider globally. @@ -2396,23 +4515,23 @@ angular.module( 'ui.bootstrap.tooltip', [ 'ui.bootstrap.position', 'ui.bootstrap * $tooltipProvider.options( { placement: 'left' } ); * }); */ - this.options = function( value ) { - angular.extend( globalOptions, value ); + this.options = function(value) { + angular.extend(globalOptions, value); }; /** * This allows you to extend the set of trigger mappings available. E.g.: * - * $tooltipProvider.setTriggers( 'openTrigger': 'closeTrigger' ); + * $tooltipProvider.setTriggers( { 'openTrigger': 'closeTrigger' } ); */ - this.setTriggers = function setTriggers ( triggers ) { - angular.extend( triggerMap, triggers ); + this.setTriggers = function setTriggers(triggers) { + angular.extend(triggerMap, triggers); }; /** - * This is a helper function for translating camel-case to snake-case. + * This is a helper function for translating camel-case to snake_case. */ - function snake_case(name){ + function snake_case(name) { var regexp = /[A-Z]/g; var separator = '-'; return name.replace(regexp, function(letter, pos) { @@ -2424,9 +4543,27 @@ angular.module( 'ui.bootstrap.tooltip', [ 'ui.bootstrap.position', 'ui.bootstrap * Returns the actual instance of the $tooltip service. * TODO support multiple triggers */ - this.$get = [ '$window', '$compile', '$timeout', '$parse', '$document', '$position', '$interpolate', function ( $window, $compile, $timeout, $parse, $document, $position, $interpolate ) { - return function $tooltip ( type, prefix, defaultTriggerShow ) { - var options = angular.extend( {}, defaultOptions, globalOptions ); + this.$get = ['$window', '$compile', '$timeout', '$document', '$uibPosition', '$interpolate', '$rootScope', '$parse', '$$stackedMap', function($window, $compile, $timeout, $document, $position, $interpolate, $rootScope, $parse, $$stackedMap) { + var openedTooltips = $$stackedMap.createNew(); + $document.on('keypress', keypressListener); + + $rootScope.$on('$destroy', function() { + $document.off('keypress', keypressListener); + }); + + function keypressListener(e) { + if (e.which === 27) { + var last = openedTooltips.top(); + if (last) { + last.value.close(); + openedTooltips.removeTop(); + last = null; + } + } + } + + return function $tooltip(ttType, prefix, defaultTriggerShow, options) { + options = angular.extend({}, defaultOptions, globalOptions, options); /** * Returns an object of show and hide triggers. @@ -2442,59 +4579,104 @@ angular.module( 'ui.bootstrap.tooltip', [ 'ui.bootstrap.position', 'ui.bootstrap * undefined; otherwise, it uses the `triggerMap` value of the show * trigger; else it will just use the show trigger. */ - function getTriggers ( trigger ) { - var show = trigger || options.trigger || defaultTriggerShow; - var hide = triggerMap[show] || show; + function getTriggers(trigger) { + var show = (trigger || options.trigger || defaultTriggerShow).split(' '); + var hide = show.map(function(trigger) { + return triggerMap[trigger] || trigger; + }); return { show: show, hide: hide }; } - var directiveName = snake_case( type ); + var directiveName = snake_case(ttType); var startSym = $interpolate.startSymbol(); var endSym = $interpolate.endSymbol(); var template = - '
'+ + '
' + '
'; return { - restrict: 'EA', - scope: true, - compile: function (tElem, tAttrs) { - var tooltipLinker = $compile( template ); + compile: function(tElem, tAttrs) { + var tooltipLinker = $compile(template); - return function link ( scope, element, attrs ) { + return function link(scope, element, attrs, tooltipCtrl) { var tooltip; + var tooltipLinkedScope; var transitionTimeout; - var popupTimeout; - var appendToBody = angular.isDefined( options.appendToBody ) ? options.appendToBody : false; - var triggers = getTriggers( undefined ); - var hasEnableExp = angular.isDefined(attrs[prefix+'Enable']); + var showTimeout; + var hideTimeout; + var positionTimeout; + var appendToBody = angular.isDefined(options.appendToBody) ? options.appendToBody : false; + var triggers = getTriggers(undefined); + var hasEnableExp = angular.isDefined(attrs[prefix + 'Enable']); + var ttScope = scope.$new(true); + var repositionScheduled = false; + var isOpenParse = angular.isDefined(attrs[prefix + 'IsOpen']) ? $parse(attrs[prefix + 'IsOpen']) : false; + var contentParse = options.useContentExp ? $parse(attrs[ttType]) : false; + var observers = []; + var lastPlacement; - var positionTooltip = function () { + var positionTooltip = function() { + // check if tooltip exists and is not empty + if (!tooltip || !tooltip.html()) { return; } - var ttPosition = $position.positionElements(element, tooltip, scope.tt_placement, appendToBody); - ttPosition.top += 'px'; - ttPosition.left += 'px'; + if (!positionTimeout) { + positionTimeout = $timeout(function() { + var ttPosition = $position.positionElements(element, tooltip, ttScope.placement, appendToBody); + tooltip.css({ top: ttPosition.top + 'px', left: ttPosition.left + 'px' }); - // Now set the calculated positioning. - tooltip.css( ttPosition ); + if (!tooltip.hasClass(ttPosition.placement.split('-')[0])) { + tooltip.removeClass(lastPlacement.split('-')[0]); + tooltip.addClass(ttPosition.placement.split('-')[0]); + } + + if (!tooltip.hasClass(options.placementClassPrefix + ttPosition.placement)) { + tooltip.removeClass(options.placementClassPrefix + lastPlacement); + tooltip.addClass(options.placementClassPrefix + ttPosition.placement); + } + + // first time through tt element will have the + // uib-position-measure class or if the placement + // has changed we need to position the arrow. + if (tooltip.hasClass('uib-position-measure')) { + $position.positionArrow(tooltip, ttPosition.placement); + tooltip.removeClass('uib-position-measure'); + } else if (lastPlacement !== ttPosition.placement) { + $position.positionArrow(tooltip, ttPosition.placement); + } + lastPlacement = ttPosition.placement; + + positionTimeout = null; + }, 0, false); + } }; + // Set up the correct scope to allow transclusion later + ttScope.origScope = scope; + // By default, the tooltip is not open. // TODO add ability to start tooltip opened - scope.tt_isOpen = false; + ttScope.isOpen = false; + openedTooltips.add(ttScope, { + close: hide + }); - function toggleTooltipBind () { - if ( ! scope.tt_isOpen ) { + function toggleTooltipBind() { + if (!ttScope.isOpen) { showTooltipBind(); } else { hideTooltipBind(); @@ -2503,174 +4685,338 @@ angular.module( 'ui.bootstrap.tooltip', [ 'ui.bootstrap.position', 'ui.bootstrap // Show the tooltip with delay if specified, otherwise show it immediately function showTooltipBind() { - if(hasEnableExp && !scope.$eval(attrs[prefix+'Enable'])) { + if (hasEnableExp && !scope.$eval(attrs[prefix + 'Enable'])) { return; } - if ( scope.tt_popupDelay ) { + + cancelHide(); + prepareTooltip(); + + if (ttScope.popupDelay) { // Do nothing if the tooltip was already scheduled to pop-up. // This happens if show is triggered multiple times before any hide is triggered. - if (!popupTimeout) { - popupTimeout = $timeout( show, scope.tt_popupDelay, false ); - popupTimeout.then(function(reposition){reposition();}); + if (!showTimeout) { + showTimeout = $timeout(show, ttScope.popupDelay, false); } } else { - show()(); + show(); } } - function hideTooltipBind () { - scope.$apply(function () { + function hideTooltipBind() { + cancelShow(); + + if (ttScope.popupCloseDelay) { + if (!hideTimeout) { + hideTimeout = $timeout(hide, ttScope.popupCloseDelay, false); + } + } else { hide(); - }); + } } // Show the tooltip popup element. function show() { - - popupTimeout = null; - - // If there is a pending remove transition, we must cancel it, lest the - // tooltip be mysteriously removed. - if ( transitionTimeout ) { - $timeout.cancel( transitionTimeout ); - transitionTimeout = null; - } + cancelShow(); + cancelHide(); // Don't show empty tooltips. - if ( ! scope.tt_content ) { + if (!ttScope.content) { return angular.noop; } createTooltip(); - // Set the initial positioning. - tooltip.css({ top: 0, left: 0, display: 'block' }); + // And show the tooltip. + ttScope.$evalAsync(function() { + ttScope.isOpen = true; + assignIsOpen(true); + positionTooltip(); + }); + } - // Now we add it to the DOM because need some info about it. But it's not - // visible yet anyway. - if ( appendToBody ) { - $document.find( 'body' ).append( tooltip ); - } else { - element.after( tooltip ); + function cancelShow() { + if (showTimeout) { + $timeout.cancel(showTimeout); + showTimeout = null; } - positionTooltip(); - - // And show the tooltip. - scope.tt_isOpen = true; - scope.$digest(); // digest required as $apply is not called - - // Return positioning function as promise callback for correct - // positioning after draw. - return positionTooltip; + if (positionTimeout) { + $timeout.cancel(positionTimeout); + positionTimeout = null; + } } // Hide the tooltip popup element. function hide() { + if (!ttScope) { + return; + } + // First things first: we don't show it anymore. - scope.tt_isOpen = false; - - //if tooltip is going to be shown after delay, we must cancel this - $timeout.cancel( popupTimeout ); - popupTimeout = null; - - // And now we remove it from the DOM. However, if we have animation, we - // need to wait for it to expire beforehand. - // FIXME: this is a placeholder for a port of the transitions library. - if ( scope.tt_animation ) { - if (!transitionTimeout) { - transitionTimeout = $timeout(removeTooltip, 500); + ttScope.$evalAsync(function() { + if (ttScope) { + ttScope.isOpen = false; + assignIsOpen(false); + // And now we remove it from the DOM. However, if we have animation, we + // need to wait for it to expire beforehand. + // FIXME: this is a placeholder for a port of the transitions library. + // The fade transition in TWBS is 150ms. + if (ttScope.animation) { + if (!transitionTimeout) { + transitionTimeout = $timeout(removeTooltip, 150, false); + } + } else { + removeTooltip(); + } } - } else { - removeTooltip(); + }); + } + + function cancelHide() { + if (hideTimeout) { + $timeout.cancel(hideTimeout); + hideTimeout = null; + } + + if (transitionTimeout) { + $timeout.cancel(transitionTimeout); + transitionTimeout = null; } } function createTooltip() { // There can only be one tooltip element per directive shown at once. if (tooltip) { - removeTooltip(); + return; } - tooltip = tooltipLinker(scope, function () {}); - // Get contents rendered into the tooltip - scope.$digest(); + tooltipLinkedScope = ttScope.$new(); + tooltip = tooltipLinker(tooltipLinkedScope, function(tooltip) { + if (appendToBody) { + $document.find('body').append(tooltip); + } else { + element.after(tooltip); + } + }); + + prepObservers(); } function removeTooltip() { - transitionTimeout = null; + cancelShow(); + cancelHide(); + unregisterObservers(); + if (tooltip) { tooltip.remove(); tooltip = null; } + if (tooltipLinkedScope) { + tooltipLinkedScope.$destroy(); + tooltipLinkedScope = null; + } } + /** + * Set the initial scope values. Once + * the tooltip is created, the observers + * will be added to keep things in sync. + */ + function prepareTooltip() { + ttScope.title = attrs[prefix + 'Title']; + if (contentParse) { + ttScope.content = contentParse(scope); + } else { + ttScope.content = attrs[ttType]; + } + + ttScope.popupClass = attrs[prefix + 'Class']; + ttScope.placement = angular.isDefined(attrs[prefix + 'Placement']) ? attrs[prefix + 'Placement'] : options.placement; + var placement = $position.parsePlacement(ttScope.placement); + lastPlacement = placement[1] ? placement[0] + '-' + placement[1] : placement[0]; + + var delay = parseInt(attrs[prefix + 'PopupDelay'], 10); + var closeDelay = parseInt(attrs[prefix + 'PopupCloseDelay'], 10); + ttScope.popupDelay = !isNaN(delay) ? delay : options.popupDelay; + ttScope.popupCloseDelay = !isNaN(closeDelay) ? closeDelay : options.popupCloseDelay; + } + + function assignIsOpen(isOpen) { + if (isOpenParse && angular.isFunction(isOpenParse.assign)) { + isOpenParse.assign(scope, isOpen); + } + } + + ttScope.contentExp = function() { + return ttScope.content; + }; + /** * Observe the relevant attributes. */ - attrs.$observe( type, function ( val ) { - scope.tt_content = val; + attrs.$observe('disabled', function(val) { + if (val) { + cancelShow(); + } - if (!val && scope.tt_isOpen ) { + if (val && ttScope.isOpen) { hide(); } }); - attrs.$observe( prefix+'Title', function ( val ) { - scope.tt_title = val; - }); + if (isOpenParse) { + scope.$watch(isOpenParse, function(val) { + if (ttScope && !val === ttScope.isOpen) { + toggleTooltipBind(); + } + }); + } - attrs.$observe( prefix+'Placement', function ( val ) { - scope.tt_placement = angular.isDefined( val ) ? val : options.placement; - }); + function prepObservers() { + observers.length = 0; - attrs.$observe( prefix+'PopupDelay', function ( val ) { - var delay = parseInt( val, 10 ); - scope.tt_popupDelay = ! isNaN(delay) ? delay : options.popupDelay; - }); + if (contentParse) { + observers.push( + scope.$watch(contentParse, function(val) { + ttScope.content = val; + if (!val && ttScope.isOpen) { + hide(); + } + }) + ); - var unregisterTriggers = function () { - element.unbind(triggers.show, showTooltipBind); - element.unbind(triggers.hide, hideTooltipBind); + observers.push( + tooltipLinkedScope.$watch(function() { + if (!repositionScheduled) { + repositionScheduled = true; + tooltipLinkedScope.$$postDigest(function() { + repositionScheduled = false; + if (ttScope && ttScope.isOpen) { + positionTooltip(); + } + }); + } + }) + ); + } else { + observers.push( + attrs.$observe(ttType, function(val) { + ttScope.content = val; + if (!val && ttScope.isOpen) { + hide(); + } else { + positionTooltip(); + } + }) + ); + } + + observers.push( + attrs.$observe(prefix + 'Title', function(val) { + ttScope.title = val; + if (ttScope.isOpen) { + positionTooltip(); + } + }) + ); + + observers.push( + attrs.$observe(prefix + 'Placement', function(val) { + ttScope.placement = val ? val : options.placement; + if (ttScope.isOpen) { + positionTooltip(); + } + }) + ); + } + + function unregisterObservers() { + if (observers.length) { + angular.forEach(observers, function(observer) { + observer(); + }); + observers.length = 0; + } + } + + // hide tooltips/popovers for outsideClick trigger + function bodyHideTooltipBind(e) { + if (!ttScope || !ttScope.isOpen || !tooltip) { + return; + } + // make sure the tooltip/popover link or tool tooltip/popover itself were not clicked + if (!element[0].contains(e.target) && !tooltip[0].contains(e.target)) { + hideTooltipBind(); + } + } + + var unregisterTriggers = function() { + triggers.show.forEach(function(trigger) { + if (trigger === 'outsideClick') { + element.off('click', toggleTooltipBind); + } else { + element.off(trigger, showTooltipBind); + element.off(trigger, toggleTooltipBind); + } + }); + triggers.hide.forEach(function(trigger) { + if (trigger === 'outsideClick') { + $document.off('click', bodyHideTooltipBind); + } else { + element.off(trigger, hideTooltipBind); + } + }); }; - attrs.$observe( prefix+'Trigger', function ( val ) { + function prepTriggers() { + var val = attrs[prefix + 'Trigger']; unregisterTriggers(); - triggers = getTriggers( val ); + triggers = getTriggers(val); - if ( triggers.show === triggers.hide ) { - element.bind( triggers.show, toggleTooltipBind ); - } else { - element.bind( triggers.show, showTooltipBind ); - element.bind( triggers.hide, hideTooltipBind ); + if (triggers.show !== 'none') { + triggers.show.forEach(function(trigger, idx) { + if (trigger === 'outsideClick') { + element.on('click', toggleTooltipBind); + $document.on('click', bodyHideTooltipBind); + } else if (trigger === triggers.hide[idx]) { + element.on(trigger, toggleTooltipBind); + } else if (trigger) { + element.on(trigger, showTooltipBind); + element.on(triggers.hide[idx], hideTooltipBind); + } + + element.on('keypress', function(e) { + if (e.which === 27) { + hideTooltipBind(); + } + }); + }); } - }); + } + + prepTriggers(); var animation = scope.$eval(attrs[prefix + 'Animation']); - scope.tt_animation = angular.isDefined(animation) ? !!animation : options.animation; + ttScope.animation = angular.isDefined(animation) ? !!animation : options.animation; - attrs.$observe( prefix+'AppendToBody', function ( val ) { - appendToBody = angular.isDefined( val ) ? $parse( val )( scope ) : appendToBody; - }); - - // if a tooltip is attached to we need to remove it on - // location change as its parent scope will probably not be destroyed - // by the change. - if ( appendToBody ) { - scope.$on('$locationChangeSuccess', function closeTooltipOnLocationChangeSuccess () { - if ( scope.tt_isOpen ) { - hide(); - } - }); + var appendToBodyVal; + var appendKey = prefix + 'AppendToBody'; + if (appendKey in attrs && attrs[appendKey] === undefined) { + appendToBodyVal = true; + } else { + appendToBodyVal = scope.$eval(attrs[appendKey]); } + appendToBody = angular.isDefined(appendToBodyVal) ? appendToBodyVal : appendToBody; + // Make sure tooltip is destroyed and removed. scope.$on('$destroy', function onDestroyTooltip() { - $timeout.cancel( transitionTimeout ); - $timeout.cancel( popupTimeout ); unregisterTriggers(); removeTooltip(); + openedTooltips.remove(ttScope); + ttScope = null; }); }; } @@ -2679,172 +5025,371 @@ angular.module( 'ui.bootstrap.tooltip', [ 'ui.bootstrap.position', 'ui.bootstrap }]; }) -.directive( 'tooltipPopup', function () { +// This is mostly ngInclude code but with a custom scope +.directive('uibTooltipTemplateTransclude', [ + '$animate', '$sce', '$compile', '$templateRequest', +function ($animate, $sce, $compile, $templateRequest) { return { - restrict: 'EA', - replace: true, - scope: { content: '@', placement: '@', animation: '&', isOpen: '&' }, - templateUrl: 'template/tooltip/tooltip-popup.html' - }; -}) + link: function(scope, elem, attrs) { + var origScope = scope.$eval(attrs.tooltipTemplateTranscludeScope); -.directive( 'tooltip', [ '$tooltip', function ( $tooltip ) { - return $tooltip( 'tooltip', 'tooltip', 'mouseenter' ); + var changeCounter = 0, + currentScope, + previousElement, + currentElement; + + var cleanupLastIncludeContent = function() { + if (previousElement) { + previousElement.remove(); + previousElement = null; + } + + if (currentScope) { + currentScope.$destroy(); + currentScope = null; + } + + if (currentElement) { + $animate.leave(currentElement).then(function() { + previousElement = null; + }); + previousElement = currentElement; + currentElement = null; + } + }; + + scope.$watch($sce.parseAsResourceUrl(attrs.uibTooltipTemplateTransclude), function(src) { + var thisChangeId = ++changeCounter; + + if (src) { + //set the 2nd param to true to ignore the template request error so that the inner + //contents and scope can be cleaned up. + $templateRequest(src, true).then(function(response) { + if (thisChangeId !== changeCounter) { return; } + var newScope = origScope.$new(); + var template = response; + + var clone = $compile(template)(newScope, function(clone) { + cleanupLastIncludeContent(); + $animate.enter(clone, elem); + }); + + currentScope = newScope; + currentElement = clone; + + currentScope.$emit('$includeContentLoaded', src); + }, function() { + if (thisChangeId === changeCounter) { + cleanupLastIncludeContent(); + scope.$emit('$includeContentError', src); + } + }); + scope.$emit('$includeContentRequested', src); + } else { + cleanupLastIncludeContent(); + } + }); + + scope.$on('$destroy', cleanupLastIncludeContent); + } + }; }]) -.directive( 'tooltipHtmlUnsafePopup', function () { +/** + * Note that it's intentional that these classes are *not* applied through $animate. + * They must not be animated as they're expected to be present on the tooltip on + * initialization. + */ +.directive('uibTooltipClasses', ['$uibPosition', function($uibPosition) { + return { + restrict: 'A', + link: function(scope, element, attrs) { + // need to set the primary position so the + // arrow has space during position measure. + // tooltip.positionTooltip() + if (scope.placement) { + // // There are no top-left etc... classes + // // in TWBS, so we need the primary position. + var position = $uibPosition.parsePlacement(scope.placement); + element.addClass(position[0]); + } + + if (scope.popupClass) { + element.addClass(scope.popupClass); + } + + if (scope.animation()) { + element.addClass(attrs.tooltipAnimationClass); + } + } + }; +}]) + +.directive('uibTooltipPopup', function() { return { - restrict: 'EA', replace: true, - scope: { content: '@', placement: '@', animation: '&', isOpen: '&' }, - templateUrl: 'template/tooltip/tooltip-html-unsafe-popup.html' + scope: { content: '@', placement: '@', popupClass: '@', animation: '&', isOpen: '&' }, + templateUrl: 'uib/template/tooltip/tooltip-popup.html' }; }) -.directive( 'tooltipHtmlUnsafe', [ '$tooltip', function ( $tooltip ) { - return $tooltip( 'tooltipHtmlUnsafe', 'tooltip', 'mouseenter' ); +.directive('uibTooltip', [ '$uibTooltip', function($uibTooltip) { + return $uibTooltip('uibTooltip', 'tooltip', 'mouseenter'); +}]) + +.directive('uibTooltipTemplatePopup', function() { + return { + replace: true, + scope: { contentExp: '&', placement: '@', popupClass: '@', animation: '&', isOpen: '&', + originScope: '&' }, + templateUrl: 'uib/template/tooltip/tooltip-template-popup.html' + }; +}) + +.directive('uibTooltipTemplate', ['$uibTooltip', function($uibTooltip) { + return $uibTooltip('uibTooltipTemplate', 'tooltip', 'mouseenter', { + useContentExp: true + }); +}]) + +.directive('uibTooltipHtmlPopup', function() { + return { + replace: true, + scope: { contentExp: '&', placement: '@', popupClass: '@', animation: '&', isOpen: '&' }, + templateUrl: 'uib/template/tooltip/tooltip-html-popup.html' + }; +}) + +.directive('uibTooltipHtml', ['$uibTooltip', function($uibTooltip) { + return $uibTooltip('uibTooltipHtml', 'tooltip', 'mouseenter', { + useContentExp: true + }); }]); /** * The following features are still outstanding: popup delay, animation as a * function, placement as a function, inside, support for more triggers than - * just mouse enter/leave, html popovers, and selector delegatation. + * just mouse enter/leave, and selector delegatation. */ -angular.module( 'ui.bootstrap.popover', [ 'ui.bootstrap.tooltip' ] ) +angular.module('ui.bootstrap.popover', ['ui.bootstrap.tooltip']) -.directive( 'popoverPopup', function () { +.directive('uibPopoverTemplatePopup', function() { return { - restrict: 'EA', replace: true, - scope: { title: '@', content: '@', placement: '@', animation: '&', isOpen: '&' }, - templateUrl: 'template/popover/popover.html' + scope: { uibTitle: '@', contentExp: '&', placement: '@', popupClass: '@', animation: '&', isOpen: '&', + originScope: '&' }, + templateUrl: 'uib/template/popover/popover-template.html' }; }) -.directive( 'popover', [ '$tooltip', function ( $tooltip ) { - return $tooltip( 'popover', 'popover', 'click' ); +.directive('uibPopoverTemplate', ['$uibTooltip', function($uibTooltip) { + return $uibTooltip('uibPopoverTemplate', 'popover', 'click', { + useContentExp: true + }); +}]) + +.directive('uibPopoverHtmlPopup', function() { + return { + replace: true, + scope: { contentExp: '&', uibTitle: '@', placement: '@', popupClass: '@', animation: '&', isOpen: '&' }, + templateUrl: 'uib/template/popover/popover-html.html' + }; +}) + +.directive('uibPopoverHtml', ['$uibTooltip', function($uibTooltip) { + return $uibTooltip('uibPopoverHtml', 'popover', 'click', { + useContentExp: true + }); +}]) + +.directive('uibPopoverPopup', function() { + return { + replace: true, + scope: { uibTitle: '@', content: '@', placement: '@', popupClass: '@', animation: '&', isOpen: '&' }, + templateUrl: 'uib/template/popover/popover.html' + }; +}) + +.directive('uibPopover', ['$uibTooltip', function($uibTooltip) { + return $uibTooltip('uibPopover', 'popover', 'click'); }]); angular.module('ui.bootstrap.progressbar', []) -.constant('progressConfig', { +.constant('uibProgressConfig', { animate: true, max: 100 }) -.controller('ProgressController', ['$scope', '$attrs', 'progressConfig', function($scope, $attrs, progressConfig) { - var self = this, - animate = angular.isDefined($attrs.animate) ? $scope.$parent.$eval($attrs.animate) : progressConfig.animate; +.controller('UibProgressController', ['$scope', '$attrs', 'uibProgressConfig', function($scope, $attrs, progressConfig) { + var self = this, + animate = angular.isDefined($attrs.animate) ? $scope.$parent.$eval($attrs.animate) : progressConfig.animate; - this.bars = []; - $scope.max = angular.isDefined($attrs.max) ? $scope.$parent.$eval($attrs.max) : progressConfig.max; + this.bars = []; + $scope.max = getMaxOrDefault(); - this.addBar = function(bar, element) { - if ( !animate ) { - element.css({'transition': 'none'}); - } + this.addBar = function(bar, element, attrs) { + if (!animate) { + element.css({'transition': 'none'}); + } - this.bars.push(bar); + this.bars.push(bar); - bar.$watch('value', function( value ) { - bar.percent = +(100 * value / $scope.max).toFixed(2); - }); + bar.max = getMaxOrDefault(); + bar.title = attrs && angular.isDefined(attrs.title) ? attrs.title : 'progressbar'; - bar.$on('$destroy', function() { - element = null; - self.removeBar(bar); - }); + bar.$watch('value', function(value) { + bar.recalculatePercentage(); + }); + + bar.recalculatePercentage = function() { + var totalPercentage = self.bars.reduce(function(total, bar) { + bar.percent = +(100 * bar.value / bar.max).toFixed(2); + return total + bar.percent; + }, 0); + + if (totalPercentage > 100) { + bar.percent -= totalPercentage - 100; + } }; - this.removeBar = function(bar) { - this.bars.splice(this.bars.indexOf(bar), 1); - }; + bar.$on('$destroy', function() { + element = null; + self.removeBar(bar); + }); + }; + + this.removeBar = function(bar) { + this.bars.splice(this.bars.indexOf(bar), 1); + this.bars.forEach(function (bar) { + bar.recalculatePercentage(); + }); + }; + + //$attrs.$observe('maxParam', function(maxParam) { + $scope.$watch('maxParam', function(maxParam) { + self.bars.forEach(function(bar) { + bar.max = getMaxOrDefault(); + bar.recalculatePercentage(); + }); + }); + + function getMaxOrDefault () { + return angular.isDefined($scope.maxParam) ? $scope.maxParam : progressConfig.max; + } }]) -.directive('progress', function() { - return { - restrict: 'EA', - replace: true, - transclude: true, - controller: 'ProgressController', - require: 'progress', - scope: {}, - templateUrl: 'template/progressbar/progress.html' - }; +.directive('uibProgress', function() { + return { + replace: true, + transclude: true, + controller: 'UibProgressController', + require: 'uibProgress', + scope: { + maxParam: '=?max' + }, + templateUrl: 'uib/template/progressbar/progress.html' + }; }) -.directive('bar', function() { - return { - restrict: 'EA', - replace: true, - transclude: true, - require: '^progress', - scope: { - value: '=', - type: '@' - }, - templateUrl: 'template/progressbar/bar.html', - link: function(scope, element, attrs, progressCtrl) { - progressCtrl.addBar(scope, element); - } - }; +.directive('uibBar', function() { + return { + replace: true, + transclude: true, + require: '^uibProgress', + scope: { + value: '=', + type: '@' + }, + templateUrl: 'uib/template/progressbar/bar.html', + link: function(scope, element, attrs, progressCtrl) { + progressCtrl.addBar(scope, element, attrs); + } + }; }) -.directive('progressbar', function() { - return { - restrict: 'EA', - replace: true, - transclude: true, - controller: 'ProgressController', - scope: { - value: '=', - type: '@' - }, - templateUrl: 'template/progressbar/progressbar.html', - link: function(scope, element, attrs, progressCtrl) { - progressCtrl.addBar(scope, angular.element(element.children()[0])); - } - }; +.directive('uibProgressbar', function() { + return { + replace: true, + transclude: true, + controller: 'UibProgressController', + scope: { + value: '=', + maxParam: '=?max', + type: '@' + }, + templateUrl: 'uib/template/progressbar/progressbar.html', + link: function(scope, element, attrs, progressCtrl) { + progressCtrl.addBar(scope, angular.element(element.children()[0]), {title: attrs.title}); + } + }; }); + angular.module('ui.bootstrap.rating', []) -.constant('ratingConfig', { +.constant('uibRatingConfig', { max: 5, stateOn: null, - stateOff: null + stateOff: null, + enableReset: true, + titles : ['one', 'two', 'three', 'four', 'five'] }) -.controller('RatingController', ['$scope', '$attrs', 'ratingConfig', function($scope, $attrs, ratingConfig) { - var ngModelCtrl = { $setViewValue: angular.noop }; +.controller('UibRatingController', ['$scope', '$attrs', 'uibRatingConfig', function($scope, $attrs, ratingConfig) { + var ngModelCtrl = { $setViewValue: angular.noop }, + self = this; this.init = function(ngModelCtrl_) { ngModelCtrl = ngModelCtrl_; ngModelCtrl.$render = this.render; + ngModelCtrl.$formatters.push(function(value) { + if (angular.isNumber(value) && value << 0 !== value) { + value = Math.round(value); + } + + return value; + }); + this.stateOn = angular.isDefined($attrs.stateOn) ? $scope.$parent.$eval($attrs.stateOn) : ratingConfig.stateOn; this.stateOff = angular.isDefined($attrs.stateOff) ? $scope.$parent.$eval($attrs.stateOff) : ratingConfig.stateOff; + this.enableReset = angular.isDefined($attrs.enableReset) ? + $scope.$parent.$eval($attrs.enableReset) : ratingConfig.enableReset; + var tmpTitles = angular.isDefined($attrs.titles) ? $scope.$parent.$eval($attrs.titles) : ratingConfig.titles; + this.titles = angular.isArray(tmpTitles) && tmpTitles.length > 0 ? + tmpTitles : ratingConfig.titles; - var ratingStates = angular.isDefined($attrs.ratingStates) ? $scope.$parent.$eval($attrs.ratingStates) : - new Array( angular.isDefined($attrs.max) ? $scope.$parent.$eval($attrs.max) : ratingConfig.max ); + var ratingStates = angular.isDefined($attrs.ratingStates) ? + $scope.$parent.$eval($attrs.ratingStates) : + new Array(angular.isDefined($attrs.max) ? $scope.$parent.$eval($attrs.max) : ratingConfig.max); $scope.range = this.buildTemplateObjects(ratingStates); }; this.buildTemplateObjects = function(states) { for (var i = 0, n = states.length; i < n; i++) { - states[i] = angular.extend({ index: i }, { stateOn: this.stateOn, stateOff: this.stateOff }, states[i]); + states[i] = angular.extend({ index: i }, { stateOn: this.stateOn, stateOff: this.stateOff, title: this.getTitle(i) }, states[i]); } return states; }; + this.getTitle = function(index) { + if (index >= this.titles.length) { + return index + 1; + } + + return this.titles[index]; + }; + $scope.rate = function(value) { - if ( !$scope.readonly && value >= 0 && value <= $scope.range.length ) { - ngModelCtrl.$setViewValue(value); + if (!$scope.readonly && value >= 0 && value <= $scope.range.length) { + var newViewValue = self.enableReset && ngModelCtrl.$viewValue === value ? 0 : value; + ngModelCtrl.$setViewValue(newViewValue); ngModelCtrl.$render(); } }; $scope.enter = function(value) { - if ( !$scope.readonly ) { + if (!$scope.readonly) { $scope.value = value; } $scope.onHover({value: value}); @@ -2859,222 +5404,169 @@ angular.module('ui.bootstrap.rating', []) if (/(37|38|39|40)/.test(evt.which)) { evt.preventDefault(); evt.stopPropagation(); - $scope.rate( $scope.value + (evt.which === 38 || evt.which === 39 ? 1 : -1) ); + $scope.rate($scope.value + (evt.which === 38 || evt.which === 39 ? 1 : -1)); } }; this.render = function() { $scope.value = ngModelCtrl.$viewValue; + $scope.title = self.getTitle($scope.value - 1); }; }]) -.directive('rating', function() { +.directive('uibRating', function() { return { - restrict: 'EA', - require: ['rating', 'ngModel'], + require: ['uibRating', 'ngModel'], scope: { - readonly: '=?', + readonly: '=?readOnly', onHover: '&', onLeave: '&' }, - controller: 'RatingController', - templateUrl: 'template/rating/rating.html', + controller: 'UibRatingController', + templateUrl: 'uib/template/rating/rating.html', replace: true, link: function(scope, element, attrs, ctrls) { var ratingCtrl = ctrls[0], ngModelCtrl = ctrls[1]; - - if ( ngModelCtrl ) { - ratingCtrl.init( ngModelCtrl ); - } + ratingCtrl.init(ngModelCtrl); } }; }); -/** - * @ngdoc overview - * @name ui.bootstrap.tabs - * - * @description - * AngularJS version of the tabs directive. - */ - angular.module('ui.bootstrap.tabs', []) -.controller('TabsetController', ['$scope', function TabsetCtrl($scope) { +.controller('UibTabsetController', ['$scope', function ($scope) { var ctrl = this, - tabs = ctrl.tabs = $scope.tabs = []; + oldIndex; + ctrl.tabs = []; - ctrl.select = function(selectedTab) { - angular.forEach(tabs, function(tab) { - if (tab.active && tab !== selectedTab) { - tab.active = false; - tab.onDeselect(); + ctrl.select = function(index, evt) { + if (!destroyed) { + var previousIndex = findTabIndex(oldIndex); + var previousSelected = ctrl.tabs[previousIndex]; + if (previousSelected) { + previousSelected.tab.onDeselect({ + $event: evt + }); + if (evt && evt.isDefaultPrevented()) { + return; + } + previousSelected.tab.active = false; } - }); - selectedTab.active = true; - selectedTab.onSelect(); + + var selected = ctrl.tabs[index]; + if (selected) { + selected.tab.onSelect({ + $event: evt + }); + selected.tab.active = true; + ctrl.active = selected.index; + oldIndex = selected.index; + } else if (!selected && angular.isNumber(oldIndex)) { + ctrl.active = null; + oldIndex = null; + } + } }; ctrl.addTab = function addTab(tab) { - tabs.push(tab); - // we can't run the select function on the first tab - // since that would select it twice - if (tabs.length === 1) { - tab.active = true; - } else if (tab.active) { - ctrl.select(tab); + ctrl.tabs.push({ + tab: tab, + index: tab.index + }); + ctrl.tabs.sort(function(t1, t2) { + if (t1.index > t2.index) { + return 1; + } + + if (t1.index < t2.index) { + return -1; + } + + return 0; + }); + + if (tab.index === ctrl.active || !angular.isNumber(ctrl.active) && ctrl.tabs.length === 1) { + var newActiveIndex = findTabIndex(tab.index); + ctrl.select(newActiveIndex); } }; ctrl.removeTab = function removeTab(tab) { - var index = tabs.indexOf(tab); - //Select a new tab if the tab to be removed is selected - if (tab.active && tabs.length > 1) { - //If this is the last tab, select the previous tab. else, the next tab. - var newActiveIndex = index == tabs.length - 1 ? index - 1 : index + 1; - ctrl.select(tabs[newActiveIndex]); + var index; + for (var i = 0; i < ctrl.tabs.length; i++) { + if (ctrl.tabs[i].tab === tab) { + index = i; + break; + } } - tabs.splice(index, 1); + + if (ctrl.tabs[index].index === ctrl.active) { + var newActiveTabIndex = index === ctrl.tabs.length - 1 ? + index - 1 : index + 1 % ctrl.tabs.length; + ctrl.select(newActiveTabIndex); + } + + ctrl.tabs.splice(index, 1); }; + + $scope.$watch('tabset.active', function(val) { + if (angular.isNumber(val) && val !== oldIndex) { + ctrl.select(findTabIndex(val)); + } + }); + + var destroyed; + $scope.$on('$destroy', function() { + destroyed = true; + }); + + function findTabIndex(index) { + for (var i = 0; i < ctrl.tabs.length; i++) { + if (ctrl.tabs[i].index === index) { + return i; + } + } + } }]) -/** - * @ngdoc directive - * @name ui.bootstrap.tabs.directive:tabset - * @restrict EA - * - * @description - * Tabset is the outer container for the tabs directive - * - * @param {boolean=} vertical Whether or not to use vertical styling for the tabs. - * @param {boolean=} justified Whether or not to use justified styling for the tabs. - * - * @example - - - - First Content! - Second Content! - -
- - First Vertical Content! - Second Vertical Content! - - - First Justified Content! - Second Justified Content! - -
-
- */ -.directive('tabset', function() { +.directive('uibTabset', function() { return { - restrict: 'EA', transclude: true, replace: true, - scope: { + scope: {}, + bindToController: { + active: '=?', type: '@' }, - controller: 'TabsetController', - templateUrl: 'template/tabs/tabset.html', + controller: 'UibTabsetController', + controllerAs: 'tabset', + templateUrl: function(element, attrs) { + return attrs.templateUrl || 'uib/template/tabs/tabset.html'; + }, link: function(scope, element, attrs) { - scope.vertical = angular.isDefined(attrs.vertical) ? scope.$parent.$eval(attrs.vertical) : false; - scope.justified = angular.isDefined(attrs.justified) ? scope.$parent.$eval(attrs.justified) : false; + scope.vertical = angular.isDefined(attrs.vertical) ? + scope.$parent.$eval(attrs.vertical) : false; + scope.justified = angular.isDefined(attrs.justified) ? + scope.$parent.$eval(attrs.justified) : false; + if (angular.isUndefined(attrs.active)) { + scope.active = 0; + } } }; }) -/** - * @ngdoc directive - * @name ui.bootstrap.tabs.directive:tab - * @restrict EA - * - * @param {string=} heading The visible heading, or title, of the tab. Set HTML headings with {@link ui.bootstrap.tabs.directive:tabHeading tabHeading}. - * @param {string=} select An expression to evaluate when the tab is selected. - * @param {boolean=} active A binding, telling whether or not this tab is selected. - * @param {boolean=} disabled A binding, telling whether or not this tab is disabled. - * - * @description - * Creates a tab with a heading and content. Must be placed within a {@link ui.bootstrap.tabs.directive:tabset tabset}. - * - * @example - - -
- - -
- - First Tab - - Alert me! - Second Tab, with alert callback and html heading! - - - {{item.content}} - - -
-
- - function TabsDemoCtrl($scope) { - $scope.items = [ - { title:"Dynamic Title 1", content:"Dynamic Item 0" }, - { title:"Dynamic Title 2", content:"Dynamic Item 1", disabled: true } - ]; - - $scope.alertMe = function() { - setTimeout(function() { - alert("You've selected the alert tab!"); - }); - }; - }; - -
- */ - -/** - * @ngdoc directive - * @name ui.bootstrap.tabs.directive:tabHeading - * @restrict EA - * - * @description - * Creates an HTML heading for a {@link ui.bootstrap.tabs.directive:tab tab}. Must be placed as a child of a tab element. - * - * @example - - - - - HTML in my titles?! - And some content, too! - - - Icon heading?!? - That's right. - - - - - */ -.directive('tab', ['$parse', function($parse) { +.directive('uibTab', ['$parse', function($parse) { return { - require: '^tabset', - restrict: 'EA', + require: '^uibTabset', replace: true, - templateUrl: 'template/tabs/tab.html', + templateUrl: function(element, attrs) { + return attrs.templateUrl || 'uib/template/tabs/tab.html'; + }, transclude: true, scope: { - active: '=?', heading: '@', + index: '=?', + classes: '@?', onSelect: '&select', //This callback is called in contentHeadingTransclude //once it inserts the tab's content into the dom onDeselect: '&deselect' @@ -3082,45 +5574,58 @@ angular.module('ui.bootstrap.tabs', []) controller: function() { //Empty controller so other directives can require being 'under' a tab }, - compile: function(elm, attrs, transclude) { - return function postLink(scope, elm, attrs, tabsetCtrl) { - scope.$watch('active', function(active) { - if (active) { - tabsetCtrl.select(scope); - } + controllerAs: 'tab', + link: function(scope, elm, attrs, tabsetCtrl, transclude) { + scope.disabled = false; + if (attrs.disable) { + scope.$parent.$watch($parse(attrs.disable), function(value) { + scope.disabled = !! value; }); + } - scope.disabled = false; - if ( attrs.disabled ) { - scope.$parent.$watch($parse(attrs.disabled), function(value) { - scope.disabled = !! value; - }); + if (angular.isUndefined(attrs.index)) { + if (tabsetCtrl.tabs && tabsetCtrl.tabs.length) { + scope.index = Math.max.apply(null, tabsetCtrl.tabs.map(function(t) { return t.index; })) + 1; + } else { + scope.index = 0; } + } - scope.select = function() { - if ( !scope.disabled ) { - scope.active = true; + if (angular.isUndefined(attrs.classes)) { + scope.classes = ''; + } + + scope.select = function(evt) { + if (!scope.disabled) { + var index; + for (var i = 0; i < tabsetCtrl.tabs.length; i++) { + if (tabsetCtrl.tabs[i].tab === scope) { + index = i; + break; + } } - }; - tabsetCtrl.addTab(scope); - scope.$on('$destroy', function() { - tabsetCtrl.removeTab(scope); - }); - - //We need to transclude later, once the content container is ready. - //when this link happens, we're inside a tab heading. - scope.$transcludeFn = transclude; + tabsetCtrl.select(index, evt); + } }; + + tabsetCtrl.addTab(scope); + scope.$on('$destroy', function() { + tabsetCtrl.removeTab(scope); + }); + + //We need to transclude later, once the content container is ready. + //when this link happens, we're inside a tab heading. + scope.$transcludeFn = transclude; } }; }]) -.directive('tabHeadingTransclude', [function() { +.directive('uibTabHeadingTransclude', function() { return { restrict: 'A', - require: '^tab', - link: function(scope, elm, attrs, tabCtrl) { + require: '^uibTab', + link: function(scope, elm) { scope.$watch('headingElement', function updateHeadingElement(heading) { if (heading) { elm.html(''); @@ -3129,14 +5634,14 @@ angular.module('ui.bootstrap.tabs', []) }); } }; -}]) +}) -.directive('tabContentTransclude', function() { +.directive('uibTabContentTransclude', function() { return { restrict: 'A', - require: '^tabset', + require: '^uibTabset', link: function(scope, elm, attrs) { - var tab = scope.$eval(attrs.tabContentTransclude); + var tab = scope.$eval(attrs.uibTabContentTransclude).tab; //Now our tab is ready to be transcluded: both the tab heading area //and the tab content area are loaded. Transclude 'em both. @@ -3152,96 +5657,197 @@ angular.module('ui.bootstrap.tabs', []) }); } }; + function isTabHeading(node) { - return node.tagName && ( - node.hasAttribute('tab-heading') || - node.hasAttribute('data-tab-heading') || - node.tagName.toLowerCase() === 'tab-heading' || - node.tagName.toLowerCase() === 'data-tab-heading' + return node.tagName && ( + node.hasAttribute('uib-tab-heading') || + node.hasAttribute('data-uib-tab-heading') || + node.hasAttribute('x-uib-tab-heading') || + node.tagName.toLowerCase() === 'uib-tab-heading' || + node.tagName.toLowerCase() === 'data-uib-tab-heading' || + node.tagName.toLowerCase() === 'x-uib-tab-heading' || + node.tagName.toLowerCase() === 'uib:tab-heading' ); } -}) - -; +}); angular.module('ui.bootstrap.timepicker', []) -.constant('timepickerConfig', { +.constant('uibTimepickerConfig', { hourStep: 1, minuteStep: 1, + secondStep: 1, showMeridian: true, + showSeconds: false, meridians: null, readonlyInput: false, - mousewheel: true + mousewheel: true, + arrowkeys: true, + showSpinners: true, + templateUrl: 'uib/template/timepicker/timepicker.html' }) -.controller('TimepickerController', ['$scope', '$attrs', '$parse', '$log', '$locale', 'timepickerConfig', function($scope, $attrs, $parse, $log, $locale, timepickerConfig) { +.controller('UibTimepickerController', ['$scope', '$element', '$attrs', '$parse', '$log', '$locale', 'uibTimepickerConfig', function($scope, $element, $attrs, $parse, $log, $locale, timepickerConfig) { var selected = new Date(), - ngModelCtrl = { $setViewValue: angular.noop }, // nullModelCtrl - meridians = angular.isDefined($attrs.meridians) ? $scope.$parent.$eval($attrs.meridians) : timepickerConfig.meridians || $locale.DATETIME_FORMATS.AMPMS; + watchers = [], + ngModelCtrl = { $setViewValue: angular.noop }, // nullModelCtrl + meridians = angular.isDefined($attrs.meridians) ? $scope.$parent.$eval($attrs.meridians) : timepickerConfig.meridians || $locale.DATETIME_FORMATS.AMPMS, + padHours = angular.isDefined($attrs.padHours) ? $scope.$parent.$eval($attrs.padHours) : true; - this.init = function( ngModelCtrl_, inputs ) { + $scope.tabindex = angular.isDefined($attrs.tabindex) ? $attrs.tabindex : 0; + $element.removeAttr('tabindex'); + + this.init = function(ngModelCtrl_, inputs) { ngModelCtrl = ngModelCtrl_; ngModelCtrl.$render = this.render; + ngModelCtrl.$formatters.unshift(function(modelValue) { + return modelValue ? new Date(modelValue) : null; + }); + var hoursInputEl = inputs.eq(0), - minutesInputEl = inputs.eq(1); + minutesInputEl = inputs.eq(1), + secondsInputEl = inputs.eq(2); var mousewheel = angular.isDefined($attrs.mousewheel) ? $scope.$parent.$eval($attrs.mousewheel) : timepickerConfig.mousewheel; - if ( mousewheel ) { - this.setupMousewheelEvents( hoursInputEl, minutesInputEl ); + + if (mousewheel) { + this.setupMousewheelEvents(hoursInputEl, minutesInputEl, secondsInputEl); + } + + var arrowkeys = angular.isDefined($attrs.arrowkeys) ? $scope.$parent.$eval($attrs.arrowkeys) : timepickerConfig.arrowkeys; + if (arrowkeys) { + this.setupArrowkeyEvents(hoursInputEl, minutesInputEl, secondsInputEl); } $scope.readonlyInput = angular.isDefined($attrs.readonlyInput) ? $scope.$parent.$eval($attrs.readonlyInput) : timepickerConfig.readonlyInput; - this.setupInputEvents( hoursInputEl, minutesInputEl ); + this.setupInputEvents(hoursInputEl, minutesInputEl, secondsInputEl); }; var hourStep = timepickerConfig.hourStep; if ($attrs.hourStep) { - $scope.$parent.$watch($parse($attrs.hourStep), function(value) { - hourStep = parseInt(value, 10); - }); + watchers.push($scope.$parent.$watch($parse($attrs.hourStep), function(value) { + hourStep = +value; + })); } var minuteStep = timepickerConfig.minuteStep; if ($attrs.minuteStep) { - $scope.$parent.$watch($parse($attrs.minuteStep), function(value) { - minuteStep = parseInt(value, 10); - }); + watchers.push($scope.$parent.$watch($parse($attrs.minuteStep), function(value) { + minuteStep = +value; + })); + } + + var min; + watchers.push($scope.$parent.$watch($parse($attrs.min), function(value) { + var dt = new Date(value); + min = isNaN(dt) ? undefined : dt; + })); + + var max; + watchers.push($scope.$parent.$watch($parse($attrs.max), function(value) { + var dt = new Date(value); + max = isNaN(dt) ? undefined : dt; + })); + + var disabled = false; + if ($attrs.ngDisabled) { + watchers.push($scope.$parent.$watch($parse($attrs.ngDisabled), function(value) { + disabled = value; + })); + } + + $scope.noIncrementHours = function() { + var incrementedSelected = addMinutes(selected, hourStep * 60); + return disabled || incrementedSelected > max || + incrementedSelected < selected && incrementedSelected < min; + }; + + $scope.noDecrementHours = function() { + var decrementedSelected = addMinutes(selected, -hourStep * 60); + return disabled || decrementedSelected < min || + decrementedSelected > selected && decrementedSelected > max; + }; + + $scope.noIncrementMinutes = function() { + var incrementedSelected = addMinutes(selected, minuteStep); + return disabled || incrementedSelected > max || + incrementedSelected < selected && incrementedSelected < min; + }; + + $scope.noDecrementMinutes = function() { + var decrementedSelected = addMinutes(selected, -minuteStep); + return disabled || decrementedSelected < min || + decrementedSelected > selected && decrementedSelected > max; + }; + + $scope.noIncrementSeconds = function() { + var incrementedSelected = addSeconds(selected, secondStep); + return disabled || incrementedSelected > max || + incrementedSelected < selected && incrementedSelected < min; + }; + + $scope.noDecrementSeconds = function() { + var decrementedSelected = addSeconds(selected, -secondStep); + return disabled || decrementedSelected < min || + decrementedSelected > selected && decrementedSelected > max; + }; + + $scope.noToggleMeridian = function() { + if (selected.getHours() < 12) { + return disabled || addMinutes(selected, 12 * 60) > max; + } + + return disabled || addMinutes(selected, -12 * 60) < min; + }; + + var secondStep = timepickerConfig.secondStep; + if ($attrs.secondStep) { + watchers.push($scope.$parent.$watch($parse($attrs.secondStep), function(value) { + secondStep = +value; + })); + } + + $scope.showSeconds = timepickerConfig.showSeconds; + if ($attrs.showSeconds) { + watchers.push($scope.$parent.$watch($parse($attrs.showSeconds), function(value) { + $scope.showSeconds = !!value; + })); } // 12H / 24H mode $scope.showMeridian = timepickerConfig.showMeridian; if ($attrs.showMeridian) { - $scope.$parent.$watch($parse($attrs.showMeridian), function(value) { + watchers.push($scope.$parent.$watch($parse($attrs.showMeridian), function(value) { $scope.showMeridian = !!value; - if ( ngModelCtrl.$error.time ) { + if (ngModelCtrl.$error.time) { // Evaluate from template var hours = getHoursFromTemplate(), minutes = getMinutesFromTemplate(); - if (angular.isDefined( hours ) && angular.isDefined( minutes )) { - selected.setHours( hours ); + if (angular.isDefined(hours) && angular.isDefined(minutes)) { + selected.setHours(hours); refresh(); } } else { updateTemplate(); } - }); + })); } // Get $scope.hours in 24H mode if valid - function getHoursFromTemplate ( ) { - var hours = parseInt( $scope.hours, 10 ); - var valid = ( $scope.showMeridian ) ? (hours > 0 && hours < 13) : (hours >= 0 && hours < 24); - if ( !valid ) { + function getHoursFromTemplate() { + var hours = +$scope.hours; + var valid = $scope.showMeridian ? hours > 0 && hours < 13 : + hours >= 0 && hours < 24; + if (!valid || $scope.hours === '') { return undefined; } - if ( $scope.showMeridian ) { - if ( hours === 12 ) { + if ($scope.showMeridian) { + if (hours === 12) { hours = 0; } - if ( $scope.meridian === meridians[1] ) { + if ($scope.meridian === meridians[1]) { hours = hours + 12; } } @@ -3249,89 +5855,213 @@ angular.module('ui.bootstrap.timepicker', []) } function getMinutesFromTemplate() { - var minutes = parseInt($scope.minutes, 10); - return ( minutes >= 0 && minutes < 60 ) ? minutes : undefined; + var minutes = +$scope.minutes; + var valid = minutes >= 0 && minutes < 60; + if (!valid || $scope.minutes === '') { + return undefined; + } + return minutes; } - function pad( value ) { - return ( angular.isDefined(value) && value.toString().length < 2 ) ? '0' + value : value; + function getSecondsFromTemplate() { + var seconds = +$scope.seconds; + return seconds >= 0 && seconds < 60 ? seconds : undefined; + } + + function pad(value, noPad) { + if (value === null) { + return ''; + } + + return angular.isDefined(value) && value.toString().length < 2 && !noPad ? + '0' + value : value.toString(); } // Respond on mousewheel spin - this.setupMousewheelEvents = function( hoursInputEl, minutesInputEl ) { + this.setupMousewheelEvents = function(hoursInputEl, minutesInputEl, secondsInputEl) { var isScrollingUp = function(e) { if (e.originalEvent) { e = e.originalEvent; } //pick correct delta variable depending on event - var delta = (e.wheelDelta) ? e.wheelDelta : -e.deltaY; - return (e.detail || delta > 0); + var delta = e.wheelDelta ? e.wheelDelta : -e.deltaY; + return e.detail || delta > 0; }; hoursInputEl.bind('mousewheel wheel', function(e) { - $scope.$apply( (isScrollingUp(e)) ? $scope.incrementHours() : $scope.decrementHours() ); + if (!disabled) { + $scope.$apply(isScrollingUp(e) ? $scope.incrementHours() : $scope.decrementHours()); + } e.preventDefault(); }); minutesInputEl.bind('mousewheel wheel', function(e) { - $scope.$apply( (isScrollingUp(e)) ? $scope.incrementMinutes() : $scope.decrementMinutes() ); + if (!disabled) { + $scope.$apply(isScrollingUp(e) ? $scope.incrementMinutes() : $scope.decrementMinutes()); + } e.preventDefault(); }); + secondsInputEl.bind('mousewheel wheel', function(e) { + if (!disabled) { + $scope.$apply(isScrollingUp(e) ? $scope.incrementSeconds() : $scope.decrementSeconds()); + } + e.preventDefault(); + }); }; - this.setupInputEvents = function( hoursInputEl, minutesInputEl ) { - if ( $scope.readonlyInput ) { + // Respond on up/down arrowkeys + this.setupArrowkeyEvents = function(hoursInputEl, minutesInputEl, secondsInputEl) { + hoursInputEl.bind('keydown', function(e) { + if (!disabled) { + if (e.which === 38) { // up + e.preventDefault(); + $scope.incrementHours(); + $scope.$apply(); + } else if (e.which === 40) { // down + e.preventDefault(); + $scope.decrementHours(); + $scope.$apply(); + } + } + }); + + minutesInputEl.bind('keydown', function(e) { + if (!disabled) { + if (e.which === 38) { // up + e.preventDefault(); + $scope.incrementMinutes(); + $scope.$apply(); + } else if (e.which === 40) { // down + e.preventDefault(); + $scope.decrementMinutes(); + $scope.$apply(); + } + } + }); + + secondsInputEl.bind('keydown', function(e) { + if (!disabled) { + if (e.which === 38) { // up + e.preventDefault(); + $scope.incrementSeconds(); + $scope.$apply(); + } else if (e.which === 40) { // down + e.preventDefault(); + $scope.decrementSeconds(); + $scope.$apply(); + } + } + }); + }; + + this.setupInputEvents = function(hoursInputEl, minutesInputEl, secondsInputEl) { + if ($scope.readonlyInput) { $scope.updateHours = angular.noop; $scope.updateMinutes = angular.noop; + $scope.updateSeconds = angular.noop; return; } - var invalidate = function(invalidHours, invalidMinutes) { - ngModelCtrl.$setViewValue( null ); + var invalidate = function(invalidHours, invalidMinutes, invalidSeconds) { + ngModelCtrl.$setViewValue(null); ngModelCtrl.$setValidity('time', false); if (angular.isDefined(invalidHours)) { $scope.invalidHours = invalidHours; } + if (angular.isDefined(invalidMinutes)) { $scope.invalidMinutes = invalidMinutes; } + + if (angular.isDefined(invalidSeconds)) { + $scope.invalidSeconds = invalidSeconds; + } }; $scope.updateHours = function() { - var hours = getHoursFromTemplate(); + var hours = getHoursFromTemplate(), + minutes = getMinutesFromTemplate(); - if ( angular.isDefined(hours) ) { - selected.setHours( hours ); - refresh( 'h' ); + ngModelCtrl.$setDirty(); + + if (angular.isDefined(hours) && angular.isDefined(minutes)) { + selected.setHours(hours); + selected.setMinutes(minutes); + if (selected < min || selected > max) { + invalidate(true); + } else { + refresh('h'); + } } else { invalidate(true); } }; hoursInputEl.bind('blur', function(e) { - if ( !$scope.invalidHours && $scope.hours < 10) { - $scope.$apply( function() { - $scope.hours = pad( $scope.hours ); + ngModelCtrl.$setTouched(); + if (modelIsEmpty()) { + makeValid(); + } else if ($scope.hours === null || $scope.hours === '') { + invalidate(true); + } else if (!$scope.invalidHours && $scope.hours < 10) { + $scope.$apply(function() { + $scope.hours = pad($scope.hours, !padHours); }); } }); $scope.updateMinutes = function() { - var minutes = getMinutesFromTemplate(); + var minutes = getMinutesFromTemplate(), + hours = getHoursFromTemplate(); - if ( angular.isDefined(minutes) ) { - selected.setMinutes( minutes ); - refresh( 'm' ); + ngModelCtrl.$setDirty(); + + if (angular.isDefined(minutes) && angular.isDefined(hours)) { + selected.setHours(hours); + selected.setMinutes(minutes); + if (selected < min || selected > max) { + invalidate(undefined, true); + } else { + refresh('m'); + } } else { invalidate(undefined, true); } }; minutesInputEl.bind('blur', function(e) { - if ( !$scope.invalidMinutes && $scope.minutes < 10 ) { + ngModelCtrl.$setTouched(); + if (modelIsEmpty()) { + makeValid(); + } else if ($scope.minutes === null) { + invalidate(undefined, true); + } else if (!$scope.invalidMinutes && $scope.minutes < 10) { + $scope.$apply(function() { + $scope.minutes = pad($scope.minutes); + }); + } + }); + + $scope.updateSeconds = function() { + var seconds = getSecondsFromTemplate(); + + ngModelCtrl.$setDirty(); + + if (angular.isDefined(seconds)) { + selected.setSeconds(seconds); + refresh('s'); + } else { + invalidate(undefined, undefined, true); + } + }; + + secondsInputEl.bind('blur', function(e) { + if (modelIsEmpty()) { + makeValid(); + } else if (!$scope.invalidSeconds && $scope.seconds < 10) { $scope.$apply( function() { - $scope.minutes = pad( $scope.minutes ); + $scope.seconds = pad($scope.seconds); }); } }); @@ -3339,489 +6069,860 @@ angular.module('ui.bootstrap.timepicker', []) }; this.render = function() { - var date = ngModelCtrl.$modelValue ? new Date( ngModelCtrl.$modelValue ) : null; + var date = ngModelCtrl.$viewValue; - if ( isNaN(date) ) { + if (isNaN(date)) { ngModelCtrl.$setValidity('time', false); $log.error('Timepicker directive: "ng-model" value must be a Date object, a number of milliseconds since 01.01.1970 or a string representing an RFC2822 or ISO 8601 date.'); } else { - if ( date ) { + if (date) { selected = date; } - makeValid(); + + if (selected < min || selected > max) { + ngModelCtrl.$setValidity('time', false); + $scope.invalidHours = true; + $scope.invalidMinutes = true; + } else { + makeValid(); + } updateTemplate(); } }; // Call internally when we know that model is valid. - function refresh( keyboardChange ) { + function refresh(keyboardChange) { makeValid(); - ngModelCtrl.$setViewValue( new Date(selected) ); - updateTemplate( keyboardChange ); + ngModelCtrl.$setViewValue(new Date(selected)); + updateTemplate(keyboardChange); } function makeValid() { ngModelCtrl.$setValidity('time', true); $scope.invalidHours = false; $scope.invalidMinutes = false; + $scope.invalidSeconds = false; } - function updateTemplate( keyboardChange ) { - var hours = selected.getHours(), minutes = selected.getMinutes(); + function updateTemplate(keyboardChange) { + if (!ngModelCtrl.$modelValue) { + $scope.hours = null; + $scope.minutes = null; + $scope.seconds = null; + $scope.meridian = meridians[0]; + } else { + var hours = selected.getHours(), + minutes = selected.getMinutes(), + seconds = selected.getSeconds(); - if ( $scope.showMeridian ) { - hours = ( hours === 0 || hours === 12 ) ? 12 : hours % 12; // Convert 24 to 12 hour system + if ($scope.showMeridian) { + hours = hours === 0 || hours === 12 ? 12 : hours % 12; // Convert 24 to 12 hour system + } + + $scope.hours = keyboardChange === 'h' ? hours : pad(hours, !padHours); + if (keyboardChange !== 'm') { + $scope.minutes = pad(minutes); + } + $scope.meridian = selected.getHours() < 12 ? meridians[0] : meridians[1]; + + if (keyboardChange !== 's') { + $scope.seconds = pad(seconds); + } + $scope.meridian = selected.getHours() < 12 ? meridians[0] : meridians[1]; } - - $scope.hours = keyboardChange === 'h' ? hours : pad(hours); - $scope.minutes = keyboardChange === 'm' ? minutes : pad(minutes); - $scope.meridian = selected.getHours() < 12 ? meridians[0] : meridians[1]; } - function addMinutes( minutes ) { - var dt = new Date( selected.getTime() + minutes * 60000 ); - selected.setHours( dt.getHours(), dt.getMinutes() ); + function addSecondsToSelected(seconds) { + selected = addSeconds(selected, seconds); refresh(); } + function addMinutes(selected, minutes) { + return addSeconds(selected, minutes*60); + } + + function addSeconds(date, seconds) { + var dt = new Date(date.getTime() + seconds * 1000); + var newDate = new Date(date); + newDate.setHours(dt.getHours(), dt.getMinutes(), dt.getSeconds()); + return newDate; + } + + function modelIsEmpty() { + return ($scope.hours === null || $scope.hours === '') && + ($scope.minutes === null || $scope.minutes === '') && + (!$scope.showSeconds || $scope.showSeconds && ($scope.seconds === null || $scope.seconds === '')); + } + + $scope.showSpinners = angular.isDefined($attrs.showSpinners) ? + $scope.$parent.$eval($attrs.showSpinners) : timepickerConfig.showSpinners; + $scope.incrementHours = function() { - addMinutes( hourStep * 60 ); + if (!$scope.noIncrementHours()) { + addSecondsToSelected(hourStep * 60 * 60); + } }; + $scope.decrementHours = function() { - addMinutes( - hourStep * 60 ); + if (!$scope.noDecrementHours()) { + addSecondsToSelected(-hourStep * 60 * 60); + } }; + $scope.incrementMinutes = function() { - addMinutes( minuteStep ); + if (!$scope.noIncrementMinutes()) { + addSecondsToSelected(minuteStep * 60); + } }; + $scope.decrementMinutes = function() { - addMinutes( - minuteStep ); + if (!$scope.noDecrementMinutes()) { + addSecondsToSelected(-minuteStep * 60); + } }; + + $scope.incrementSeconds = function() { + if (!$scope.noIncrementSeconds()) { + addSecondsToSelected(secondStep); + } + }; + + $scope.decrementSeconds = function() { + if (!$scope.noDecrementSeconds()) { + addSecondsToSelected(-secondStep); + } + }; + $scope.toggleMeridian = function() { - addMinutes( 12 * 60 * (( selected.getHours() < 12 ) ? 1 : -1) ); - }; -}]) + var minutes = getMinutesFromTemplate(), + hours = getHoursFromTemplate(); -.directive('timepicker', function () { - return { - restrict: 'EA', - require: ['timepicker', '?^ngModel'], - controller:'TimepickerController', - replace: true, - scope: {}, - templateUrl: 'template/timepicker/timepicker.html', - link: function(scope, element, attrs, ctrls) { - var timepickerCtrl = ctrls[0], ngModelCtrl = ctrls[1]; - - if ( ngModelCtrl ) { - timepickerCtrl.init( ngModelCtrl, element.find('input') ); + if (!$scope.noToggleMeridian()) { + if (angular.isDefined(minutes) && angular.isDefined(hours)) { + addSecondsToSelected(12 * 60 * (selected.getHours() < 12 ? 60 : -60)); + } else { + $scope.meridian = $scope.meridian === meridians[0] ? meridians[1] : meridians[0]; } } }; -}); -angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap.bindHtml']) + $scope.blur = function() { + ngModelCtrl.$setTouched(); + }; + + $scope.$on('$destroy', function() { + while (watchers.length) { + watchers.shift()(); + } + }); +}]) + +.directive('uibTimepicker', ['uibTimepickerConfig', function(uibTimepickerConfig) { + return { + require: ['uibTimepicker', '?^ngModel'], + controller: 'UibTimepickerController', + controllerAs: 'timepicker', + replace: true, + scope: {}, + templateUrl: function(element, attrs) { + return attrs.templateUrl || uibTimepickerConfig.templateUrl; + }, + link: function(scope, element, attrs, ctrls) { + var timepickerCtrl = ctrls[0], ngModelCtrl = ctrls[1]; + + if (ngModelCtrl) { + timepickerCtrl.init(ngModelCtrl, element.find('input')); + } + } + }; +}]); + +angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.debounce', 'ui.bootstrap.position']) /** * A helper service that can parse typeahead's syntax (string provided by users) * Extracted to a separate service for ease of unit testing */ - .factory('typeaheadParser', ['$parse', function ($parse) { - - // 00000111000000000000022200000000000000003333333333333330000000000044000 - var TYPEAHEAD_REGEXP = /^\s*(.*?)(?:\s+as\s+(.*?))?\s+for\s+(?:([\$\w][\$\w\d]*))\s+in\s+(.*)$/; - - return { - parse:function (input) { - - var match = input.match(TYPEAHEAD_REGEXP); - if (!match) { - throw new Error( - 'Expected typeahead specification in form of "_modelValue_ (as _label_)? for _item_ in _collection_"' + - ' but got "' + input + '".'); - } - - return { - itemName:match[3], - source:$parse(match[4]), - viewMapper:$parse(match[2] || match[1]), - modelMapper:$parse(match[1]) - }; - } - }; -}]) - - .directive('typeahead', ['$compile', '$parse', '$q', '$timeout', '$document', '$position', 'typeaheadParser', - function ($compile, $parse, $q, $timeout, $document, $position, typeaheadParser) { - - var HOT_KEYS = [9, 13, 27, 38, 40]; - - return { - require:'ngModel', - link:function (originalScope, element, attrs, modelCtrl) { - - //SUPPORTED ATTRIBUTES (OPTIONS) - - //minimal no of characters that needs to be entered before typeahead kicks-in - var minSearch = originalScope.$eval(attrs.typeaheadMinLength) || 1; - - //minimal wait time after last character typed before typehead kicks-in - var waitTime = originalScope.$eval(attrs.typeaheadWaitMs) || 0; - - //should it restrict model values to the ones selected from the popup only? - var isEditable = originalScope.$eval(attrs.typeaheadEditable) !== false; - - //binding to a variable that indicates if matches are being retrieved asynchronously - var isLoadingSetter = $parse(attrs.typeaheadLoading).assign || angular.noop; - - //a callback executed when a match is selected - var onSelectCallback = $parse(attrs.typeaheadOnSelect); - - var inputFormatter = attrs.typeaheadInputFormatter ? $parse(attrs.typeaheadInputFormatter) : undefined; - - var appendToBody = attrs.typeaheadAppendToBody ? originalScope.$eval(attrs.typeaheadAppendToBody) : false; - - //INTERNAL VARIABLES - - //model setter executed upon match selection - var $setModelValue = $parse(attrs.ngModel).assign; - - //expressions used by typeahead - var parserResult = typeaheadParser.parse(attrs.typeahead); - - var hasFocus; - - //create a child scope for the typeahead directive so we are not polluting original scope - //with typeahead-specific data (matches, query etc.) - var scope = originalScope.$new(); - originalScope.$on('$destroy', function(){ - scope.$destroy(); - }); - - // WAI-ARIA - var popupId = 'typeahead-' + scope.$id + '-' + Math.floor(Math.random() * 10000); - element.attr({ - 'aria-autocomplete': 'list', - 'aria-expanded': false, - 'aria-owns': popupId - }); - - //pop-up element used to display matches - var popUpEl = angular.element('
'); - popUpEl.attr({ - id: popupId, - matches: 'matches', - active: 'activeIdx', - select: 'select(activeIdx)', - query: 'query', - position: 'position' - }); - //custom item template - if (angular.isDefined(attrs.typeaheadTemplateUrl)) { - popUpEl.attr('template-url', attrs.typeaheadTemplateUrl); - } - - var resetMatches = function() { - scope.matches = []; - scope.activeIdx = -1; - element.attr('aria-expanded', false); - }; - - var getMatchId = function(index) { - return popupId + '-option-' + index; - }; - - // Indicate that the specified match is the active (pre-selected) item in the list owned by this typeahead. - // This attribute is added or removed automatically when the `activeIdx` changes. - scope.$watch('activeIdx', function(index) { - if (index < 0) { - element.removeAttr('aria-activedescendant'); - } else { - element.attr('aria-activedescendant', getMatchId(index)); + .factory('uibTypeaheadParser', ['$parse', function($parse) { + // 00000111000000000000022200000000000000003333333333333330000000000044000 + var TYPEAHEAD_REGEXP = /^\s*([\s\S]+?)(?:\s+as\s+([\s\S]+?))?\s+for\s+(?:([\$\w][\$\w\d]*))\s+in\s+([\s\S]+?)$/; + return { + parse: function(input) { + var match = input.match(TYPEAHEAD_REGEXP); + if (!match) { + throw new Error( + 'Expected typeahead specification in form of "_modelValue_ (as _label_)? for _item_ in _collection_"' + + ' but got "' + input + '".'); } + + return { + itemName: match[3], + source: $parse(match[4]), + viewMapper: $parse(match[2] || match[1]), + modelMapper: $parse(match[1]) + }; + } + }; + }]) + + .controller('UibTypeaheadController', ['$scope', '$element', '$attrs', '$compile', '$parse', '$q', '$timeout', '$document', '$window', '$rootScope', '$$debounce', '$uibPosition', 'uibTypeaheadParser', + function(originalScope, element, attrs, $compile, $parse, $q, $timeout, $document, $window, $rootScope, $$debounce, $position, typeaheadParser) { + var HOT_KEYS = [9, 13, 27, 38, 40]; + var eventDebounceTime = 200; + var modelCtrl, ngModelOptions; + //SUPPORTED ATTRIBUTES (OPTIONS) + + //minimal no of characters that needs to be entered before typeahead kicks-in + var minLength = originalScope.$eval(attrs.typeaheadMinLength); + if (!minLength && minLength !== 0) { + minLength = 1; + } + + originalScope.$watch(attrs.typeaheadMinLength, function (newVal) { + minLength = !newVal && newVal !== 0 ? 1 : newVal; + }); + + //minimal wait time after last character typed before typeahead kicks-in + var waitTime = originalScope.$eval(attrs.typeaheadWaitMs) || 0; + + //should it restrict model values to the ones selected from the popup only? + var isEditable = originalScope.$eval(attrs.typeaheadEditable) !== false; + originalScope.$watch(attrs.typeaheadEditable, function (newVal) { + isEditable = newVal !== false; + }); + + //binding to a variable that indicates if matches are being retrieved asynchronously + var isLoadingSetter = $parse(attrs.typeaheadLoading).assign || angular.noop; + + //a callback executed when a match is selected + var onSelectCallback = $parse(attrs.typeaheadOnSelect); + + //should it select highlighted popup value when losing focus? + var isSelectOnBlur = angular.isDefined(attrs.typeaheadSelectOnBlur) ? originalScope.$eval(attrs.typeaheadSelectOnBlur) : false; + + //binding to a variable that indicates if there were no results after the query is completed + var isNoResultsSetter = $parse(attrs.typeaheadNoResults).assign || angular.noop; + + var inputFormatter = attrs.typeaheadInputFormatter ? $parse(attrs.typeaheadInputFormatter) : undefined; + + var appendToBody = attrs.typeaheadAppendToBody ? originalScope.$eval(attrs.typeaheadAppendToBody) : false; + + var appendTo = attrs.typeaheadAppendTo ? + originalScope.$eval(attrs.typeaheadAppendTo) : null; + + var focusFirst = originalScope.$eval(attrs.typeaheadFocusFirst) !== false; + + //If input matches an item of the list exactly, select it automatically + var selectOnExact = attrs.typeaheadSelectOnExact ? originalScope.$eval(attrs.typeaheadSelectOnExact) : false; + + //binding to a variable that indicates if dropdown is open + var isOpenSetter = $parse(attrs.typeaheadIsOpen).assign || angular.noop; + + var showHint = originalScope.$eval(attrs.typeaheadShowHint) || false; + + //INTERNAL VARIABLES + + //model setter executed upon match selection + var parsedModel = $parse(attrs.ngModel); + var invokeModelSetter = $parse(attrs.ngModel + '($$$p)'); + var $setModelValue = function(scope, newValue) { + if (angular.isFunction(parsedModel(originalScope)) && + ngModelOptions && ngModelOptions.$options && ngModelOptions.$options.getterSetter) { + return invokeModelSetter(scope, {$$$p: newValue}); + } + + return parsedModel.assign(scope, newValue); + }; + + //expressions used by typeahead + var parserResult = typeaheadParser.parse(attrs.uibTypeahead); + + var hasFocus; + + //Used to avoid bug in iOS webview where iOS keyboard does not fire + //mousedown & mouseup events + //Issue #3699 + var selected; + + //create a child scope for the typeahead directive so we are not polluting original scope + //with typeahead-specific data (matches, query etc.) + var scope = originalScope.$new(); + var offDestroy = originalScope.$on('$destroy', function() { + scope.$destroy(); + }); + scope.$on('$destroy', offDestroy); + + // WAI-ARIA + var popupId = 'typeahead-' + scope.$id + '-' + Math.floor(Math.random() * 10000); + element.attr({ + 'aria-autocomplete': 'list', + 'aria-expanded': false, + 'aria-owns': popupId + }); + + var inputsContainer, hintInputElem; + //add read-only input to show hint + if (showHint) { + inputsContainer = angular.element('
'); + inputsContainer.css('position', 'relative'); + element.after(inputsContainer); + hintInputElem = element.clone(); + hintInputElem.attr('placeholder', ''); + hintInputElem.attr('tabindex', '-1'); + hintInputElem.val(''); + hintInputElem.css({ + 'position': 'absolute', + 'top': '0px', + 'left': '0px', + 'border-color': 'transparent', + 'box-shadow': 'none', + 'opacity': 1, + 'background': 'none 0% 0% / auto repeat scroll padding-box border-box rgb(255, 255, 255)', + 'color': '#999' }); + element.css({ + 'position': 'relative', + 'vertical-align': 'top', + 'background-color': 'transparent' + }); + inputsContainer.append(hintInputElem); + hintInputElem.after(element); + } - var getMatchesAsync = function(inputValue) { + //pop-up element used to display matches + var popUpEl = angular.element('
'); + popUpEl.attr({ + id: popupId, + matches: 'matches', + active: 'activeIdx', + select: 'select(activeIdx, evt)', + 'move-in-progress': 'moveInProgress', + query: 'query', + position: 'position', + 'assign-is-open': 'assignIsOpen(isOpen)', + debounce: 'debounceUpdate' + }); + //custom item template + if (angular.isDefined(attrs.typeaheadTemplateUrl)) { + popUpEl.attr('template-url', attrs.typeaheadTemplateUrl); + } - var locals = {$viewValue: inputValue}; - isLoadingSetter(originalScope, true); - $q.when(parserResult.source(originalScope, locals)).then(function(matches) { + if (angular.isDefined(attrs.typeaheadPopupTemplateUrl)) { + popUpEl.attr('popup-template-url', attrs.typeaheadPopupTemplateUrl); + } - //it might happen that several async queries were in progress if a user were typing fast - //but we are interested only in responses that correspond to the current view value - var onCurrentRequest = (inputValue === modelCtrl.$viewValue); - if (onCurrentRequest && hasFocus) { - if (matches.length > 0) { + var resetHint = function() { + if (showHint) { + hintInputElem.val(''); + } + }; - scope.activeIdx = 0; - scope.matches.length = 0; + var resetMatches = function() { + scope.matches = []; + scope.activeIdx = -1; + element.attr('aria-expanded', false); + resetHint(); + }; - //transform labels - for(var i=0; i index && inputValue) { + return inputValue.toUpperCase() === scope.matches[index].label.toUpperCase(); + } + + return false; + }; + + var getMatchesAsync = function(inputValue, evt) { + var locals = {$viewValue: inputValue}; + isLoadingSetter(originalScope, true); + isNoResultsSetter(originalScope, false); + $q.when(parserResult.source(originalScope, locals)).then(function(matches) { + //it might happen that several async queries were in progress if a user were typing fast + //but we are interested only in responses that correspond to the current view value + var onCurrentRequest = inputValue === modelCtrl.$viewValue; + if (onCurrentRequest && hasFocus) { + if (matches && matches.length > 0) { + scope.activeIdx = focusFirst ? 0 : -1; + isNoResultsSetter(originalScope, false); + scope.matches.length = 0; + + //transform labels + for (var i = 0; i < matches.length; i++) { + locals[parserResult.itemName] = matches[i]; + scope.matches.push({ + id: getMatchId(i), + label: parserResult.viewMapper(scope, locals), + model: matches[i] + }); } + + scope.query = inputValue; + //position pop-up with matches - we need to re-calculate its position each time we are opening a window + //with matches as a pop-up might be absolute-positioned and position of an input might have changed on a page + //due to other elements being rendered + recalculatePosition(); + + element.attr('aria-expanded', true); + + //Select the single remaining option if user input matches + if (selectOnExact && scope.matches.length === 1 && inputIsExactMatch(inputValue, 0)) { + if (angular.isNumber(scope.debounceUpdate) || angular.isObject(scope.debounceUpdate)) { + $$debounce(function() { + scope.select(0, evt); + }, angular.isNumber(scope.debounceUpdate) ? scope.debounceUpdate : scope.debounceUpdate['default']); + } else { + scope.select(0, evt); + } + } + + if (showHint) { + var firstLabel = scope.matches[0].label; + if (angular.isString(inputValue) && + inputValue.length > 0 && + firstLabel.slice(0, inputValue.length).toUpperCase() === inputValue.toUpperCase()) { + hintInputElem.val(inputValue + firstLabel.slice(inputValue.length)); + } else { + hintInputElem.val(''); + } + } + } else { + resetMatches(); + isNoResultsSetter(originalScope, true); } - if (onCurrentRequest) { - isLoadingSetter(originalScope, false); - } - }, function(){ - resetMatches(); + } + if (onCurrentRequest) { isLoadingSetter(originalScope, false); - }); - }; + } + }, function() { + resetMatches(); + isLoadingSetter(originalScope, false); + isNoResultsSetter(originalScope, true); + }); + }; + + // bind events only if appendToBody params exist - performance feature + if (appendToBody) { + angular.element($window).on('resize', fireRecalculating); + $document.find('body').on('scroll', fireRecalculating); + } + + // Declare the debounced function outside recalculating for + // proper debouncing + var debouncedRecalculate = $$debounce(function() { + // if popup is visible + if (scope.matches.length) { + recalculatePosition(); + } + + scope.moveInProgress = false; + }, eventDebounceTime); + + // Default progress type + scope.moveInProgress = false; + + function fireRecalculating() { + if (!scope.moveInProgress) { + scope.moveInProgress = true; + scope.$digest(); + } + + debouncedRecalculate(); + } + + // recalculate actual position and set new values to scope + // after digest loop is popup in right position + function recalculatePosition() { + scope.position = appendToBody ? $position.offset(element) : $position.position(element); + scope.position.top += element.prop('offsetHeight'); + } + + //we need to propagate user's query so we can higlight matches + scope.query = undefined; + + //Declare the timeout promise var outside the function scope so that stacked calls can be cancelled later + var timeoutPromise; + + var scheduleSearchWithTimeout = function(inputValue) { + timeoutPromise = $timeout(function() { + getMatchesAsync(inputValue); + }, waitTime); + }; + + var cancelPreviousTimeout = function() { + if (timeoutPromise) { + $timeout.cancel(timeoutPromise); + } + }; + + resetMatches(); + + scope.assignIsOpen = function (isOpen) { + isOpenSetter(originalScope, isOpen); + }; + + scope.select = function(activeIdx, evt) { + //called from within the $digest() cycle + var locals = {}; + var model, item; + + selected = true; + locals[parserResult.itemName] = item = scope.matches[activeIdx].model; + model = parserResult.modelMapper(originalScope, locals); + $setModelValue(originalScope, model); + modelCtrl.$setValidity('editable', true); + modelCtrl.$setValidity('parse', true); + + onSelectCallback(originalScope, { + $item: item, + $model: model, + $label: parserResult.viewMapper(originalScope, locals), + $event: evt + }); resetMatches(); - //we need to propagate user's query so we can higlight matches - scope.query = undefined; + //return focus to the input element if a match was selected via a mouse click event + // use timeout to avoid $rootScope:inprog error + if (scope.$eval(attrs.typeaheadFocusOnSelect) !== false) { + $timeout(function() { element[0].focus(); }, 0, false); + } + }; - //Declare the timeout promise var outside the function scope so that stacked calls can be cancelled later - var timeoutPromise; + //bind keyboard events: arrows up(38) / down(40), enter(13) and tab(9), esc(27) + element.on('keydown', function(evt) { + //typeahead is open and an "interesting" key was pressed + if (scope.matches.length === 0 || HOT_KEYS.indexOf(evt.which) === -1) { + return; + } + + /** + * if there's nothing selected (i.e. focusFirst) and enter or tab is hit + * or + * shift + tab is pressed to bring focus to the previous element + * then clear the results + */ + if (scope.activeIdx === -1 && (evt.which === 9 || evt.which === 13) || evt.which === 9 && !!evt.shiftKey) { + resetMatches(); + scope.$digest(); + return; + } + + evt.preventDefault(); + var target; + switch (evt.which) { + case 9: + case 13: + scope.$apply(function () { + if (angular.isNumber(scope.debounceUpdate) || angular.isObject(scope.debounceUpdate)) { + $$debounce(function() { + scope.select(scope.activeIdx, evt); + }, angular.isNumber(scope.debounceUpdate) ? scope.debounceUpdate : scope.debounceUpdate['default']); + } else { + scope.select(scope.activeIdx, evt); + } + }); + break; + case 27: + evt.stopPropagation(); + + resetMatches(); + originalScope.$digest(); + break; + case 38: + scope.activeIdx = (scope.activeIdx > 0 ? scope.activeIdx : scope.matches.length) - 1; + scope.$digest(); + target = popUpEl.find('li')[scope.activeIdx]; + target.parentNode.scrollTop = target.offsetTop; + break; + case 40: + scope.activeIdx = (scope.activeIdx + 1) % scope.matches.length; + scope.$digest(); + target = popUpEl.find('li')[scope.activeIdx]; + target.parentNode.scrollTop = target.offsetTop; + break; + } + }); + + element.bind('focus', function (evt) { + hasFocus = true; + if (minLength === 0 && !modelCtrl.$viewValue) { + $timeout(function() { + getMatchesAsync(modelCtrl.$viewValue, evt); + }, 0); + } + }); + + element.bind('blur', function(evt) { + if (isSelectOnBlur && scope.matches.length && scope.activeIdx !== -1 && !selected) { + selected = true; + scope.$apply(function() { + if (angular.isObject(scope.debounceUpdate) && angular.isNumber(scope.debounceUpdate.blur)) { + $$debounce(function() { + scope.select(scope.activeIdx, evt); + }, scope.debounceUpdate.blur); + } else { + scope.select(scope.activeIdx, evt); + } + }); + } + if (!isEditable && modelCtrl.$error.editable) { + modelCtrl.$setViewValue(); + // Reset validity as we are clearing + modelCtrl.$setValidity('editable', true); + modelCtrl.$setValidity('parse', true); + element.val(''); + } + hasFocus = false; + selected = false; + }); + + // Keep reference to click handler to unbind it. + var dismissClickHandler = function(evt) { + // Issue #3973 + // Firefox treats right click as a click on document + if (element[0] !== evt.target && evt.which !== 3 && scope.matches.length !== 0) { + resetMatches(); + if (!$rootScope.$$phase) { + originalScope.$digest(); + } + } + }; + + $document.on('click', dismissClickHandler); + + originalScope.$on('$destroy', function() { + $document.off('click', dismissClickHandler); + if (appendToBody || appendTo) { + $popup.remove(); + } + + if (appendToBody) { + angular.element($window).off('resize', fireRecalculating); + $document.find('body').off('scroll', fireRecalculating); + } + // Prevent jQuery cache memory leak + popUpEl.remove(); + + if (showHint) { + inputsContainer.remove(); + } + }); + + var $popup = $compile(popUpEl)(scope); + + if (appendToBody) { + $document.find('body').append($popup); + } else if (appendTo) { + angular.element(appendTo).eq(0).append($popup); + } else { + element.after($popup); + } + + this.init = function(_modelCtrl, _ngModelOptions) { + modelCtrl = _modelCtrl; + ngModelOptions = _ngModelOptions; + + scope.debounceUpdate = modelCtrl.$options && $parse(modelCtrl.$options.debounce)(originalScope); //plug into $parsers pipeline to open a typeahead on view changes initiated from DOM //$parsers kick-in on all the changes coming from the view as well as manually triggered by $setViewValue - modelCtrl.$parsers.unshift(function (inputValue) { - + modelCtrl.$parsers.unshift(function(inputValue) { hasFocus = true; - if (inputValue && inputValue.length >= minSearch) { + if (minLength === 0 || inputValue && inputValue.length >= minLength) { if (waitTime > 0) { - if (timeoutPromise) { - $timeout.cancel(timeoutPromise);//cancel previous timeout - } - timeoutPromise = $timeout(function () { - getMatchesAsync(inputValue); - }, waitTime); + cancelPreviousTimeout(); + scheduleSearchWithTimeout(inputValue); } else { getMatchesAsync(inputValue); } } else { isLoadingSetter(originalScope, false); + cancelPreviousTimeout(); resetMatches(); } if (isEditable) { return inputValue; - } else { - if (!inputValue) { - // Reset in case user had typed something previously. - modelCtrl.$setValidity('editable', true); - return inputValue; - } else { - modelCtrl.$setValidity('editable', false); - return undefined; - } } + + if (!inputValue) { + // Reset in case user had typed something previously. + modelCtrl.$setValidity('editable', true); + return null; + } + + modelCtrl.$setValidity('editable', false); + return undefined; }); - modelCtrl.$formatters.push(function (modelValue) { - + modelCtrl.$formatters.push(function(modelValue) { var candidateViewValue, emptyViewValue; var locals = {}; + // The validity may be set to false via $parsers (see above) if + // the model is restricted to selected values. If the model + // is set manually it is considered to be valid. + if (!isEditable) { + modelCtrl.$setValidity('editable', true); + } + if (inputFormatter) { - - locals['$model'] = modelValue; + locals.$model = modelValue; return inputFormatter(originalScope, locals); - - } else { - - //it might happen that we don't have enough info to properly render input value - //we need to check for this situation and simply return model value if we can't apply custom formatting - locals[parserResult.itemName] = modelValue; - candidateViewValue = parserResult.viewMapper(originalScope, locals); - locals[parserResult.itemName] = undefined; - emptyViewValue = parserResult.viewMapper(originalScope, locals); - - return candidateViewValue!== emptyViewValue ? candidateViewValue : modelValue; - } - }); - - scope.select = function (activeIdx) { - //called from within the $digest() cycle - var locals = {}; - var model, item; - - locals[parserResult.itemName] = item = scope.matches[activeIdx].model; - model = parserResult.modelMapper(originalScope, locals); - $setModelValue(originalScope, model); - modelCtrl.$setValidity('editable', true); - - onSelectCallback(originalScope, { - $item: item, - $model: model, - $label: parserResult.viewMapper(originalScope, locals) - }); - - resetMatches(); - - //return focus to the input element if a match was selected via a mouse click event - // use timeout to avoid $rootScope:inprog error - $timeout(function() { element[0].focus(); }, 0, false); - }; - - //bind keyboard events: arrows up(38) / down(40), enter(13) and tab(9), esc(27) - element.bind('keydown', function (evt) { - - //typeahead is open and an "interesting" key was pressed - if (scope.matches.length === 0 || HOT_KEYS.indexOf(evt.which) === -1) { - return; } - evt.preventDefault(); + //it might happen that we don't have enough info to properly render input value + //we need to check for this situation and simply return model value if we can't apply custom formatting + locals[parserResult.itemName] = modelValue; + candidateViewValue = parserResult.viewMapper(originalScope, locals); + locals[parserResult.itemName] = undefined; + emptyViewValue = parserResult.viewMapper(originalScope, locals); - if (evt.which === 40) { - scope.activeIdx = (scope.activeIdx + 1) % scope.matches.length; - scope.$digest(); - - } else if (evt.which === 38) { - scope.activeIdx = (scope.activeIdx ? scope.activeIdx : scope.matches.length) - 1; - scope.$digest(); - - } else if (evt.which === 13 || evt.which === 9) { - scope.$apply(function () { - scope.select(scope.activeIdx); - }); - - } else if (evt.which === 27) { - evt.stopPropagation(); - - resetMatches(); - scope.$digest(); - } + return candidateViewValue !== emptyViewValue ? candidateViewValue : modelValue; }); + }; + }]) - element.bind('blur', function (evt) { - hasFocus = false; - }); - - // Keep reference to click handler to unbind it. - var dismissClickHandler = function (evt) { - if (element[0] !== evt.target) { - resetMatches(); - scope.$digest(); - } - }; - - $document.bind('click', dismissClickHandler); - - originalScope.$on('$destroy', function(){ - $document.unbind('click', dismissClickHandler); - }); - - var $popup = $compile(popUpEl)(scope); - if ( appendToBody ) { - $document.find('body').append($popup); - } else { - element.after($popup); - } - } - }; - -}]) - - .directive('typeaheadPopup', function () { + .directive('uibTypeahead', function() { return { - restrict:'EA', - scope:{ - matches:'=', - query:'=', - active:'=', - position:'=', - select:'&' - }, - replace:true, - templateUrl:'template/typeahead/typeahead-popup.html', - link:function (scope, element, attrs) { - - scope.templateUrl = attrs.templateUrl; - - scope.isOpen = function () { - return scope.matches.length > 0; - }; - - scope.isActive = function (matchIdx) { - return scope.active == matchIdx; - }; - - scope.selectActive = function (matchIdx) { - scope.active = matchIdx; - }; - - scope.selectMatch = function (activeIdx) { - scope.select({activeIdx:activeIdx}); - }; + controller: 'UibTypeaheadController', + require: ['ngModel', '^?ngModelOptions', 'uibTypeahead'], + link: function(originalScope, element, attrs, ctrls) { + ctrls[2].init(ctrls[0], ctrls[1]); } }; }) - .directive('typeaheadMatch', ['$http', '$templateCache', '$compile', '$parse', function ($http, $templateCache, $compile, $parse) { + .directive('uibTypeaheadPopup', ['$$debounce', function($$debounce) { return { - restrict:'EA', - scope:{ - index:'=', - match:'=', - query:'=' + scope: { + matches: '=', + query: '=', + active: '=', + position: '&', + moveInProgress: '=', + select: '&', + assignIsOpen: '&', + debounce: '&' }, - link:function (scope, element, attrs) { - var tplUrl = $parse(attrs.templateUrl)(scope.$parent) || 'template/typeahead/typeahead-match.html'; - $http.get(tplUrl, {cache: $templateCache}).success(function(tplContent){ - element.replaceWith($compile(tplContent.trim())(scope)); + replace: true, + templateUrl: function(element, attrs) { + return attrs.popupTemplateUrl || 'uib/template/typeahead/typeahead-popup.html'; + }, + link: function(scope, element, attrs) { + scope.templateUrl = attrs.templateUrl; + + scope.isOpen = function() { + var isDropdownOpen = scope.matches.length > 0; + scope.assignIsOpen({ isOpen: isDropdownOpen }); + return isDropdownOpen; + }; + + scope.isActive = function(matchIdx) { + return scope.active === matchIdx; + }; + + scope.selectActive = function(matchIdx) { + scope.active = matchIdx; + }; + + scope.selectMatch = function(activeIdx, evt) { + var debounce = scope.debounce(); + if (angular.isNumber(debounce) || angular.isObject(debounce)) { + $$debounce(function() { + scope.select({activeIdx: activeIdx, evt: evt}); + }, angular.isNumber(debounce) ? debounce : debounce['default']); + } else { + scope.select({activeIdx: activeIdx, evt: evt}); + } + }; + } + }; + }]) + + .directive('uibTypeaheadMatch', ['$templateRequest', '$compile', '$parse', function($templateRequest, $compile, $parse) { + return { + scope: { + index: '=', + match: '=', + query: '=' + }, + link: function(scope, element, attrs) { + var tplUrl = $parse(attrs.templateUrl)(scope.$parent) || 'uib/template/typeahead/typeahead-match.html'; + $templateRequest(tplUrl).then(function(tplContent) { + var tplEl = angular.element(tplContent.trim()); + element.replaceWith(tplEl); + $compile(tplEl)(scope); }); } }; }]) - .filter('typeaheadHighlight', function() { + .filter('uibTypeaheadHighlight', ['$sce', '$injector', '$log', function($sce, $injector, $log) { + var isSanitizePresent; + isSanitizePresent = $injector.has('$sanitize'); function escapeRegexp(queryToEscape) { + // Regex: capture the whole query string and replace it with the string that will be used to match + // the results, for example if the capture is "a" the result will be \a return queryToEscape.replace(/([.?*+^$[\]\\(){}|-])/g, '\\$1'); } - return function(matchItem, query) { - return query ? ('' + matchItem).replace(new RegExp(escapeRegexp(query), 'gi'), '$&') : matchItem; - }; - }); + function containsHtml(matchItem) { + return /<.*>/g.test(matchItem); + } -angular.module("template/accordion/accordion-group.html", []).run(["$templateCache", function($templateCache) { - $templateCache.put("template/accordion/accordion-group.html", - "
\n" + - "
\n" + + return function(matchItem, query) { + if (!isSanitizePresent && containsHtml(matchItem)) { + $log.warn('Unsafe use of typeahead please use ngSanitize'); // Warn the user about the danger + } + matchItem = query ? ('' + matchItem).replace(new RegExp(escapeRegexp(query), 'gi'), '$&') : matchItem; // Replaces the capture string with a the same string inside of a "strong" tag + if (!isSanitizePresent) { + matchItem = $sce.trustAsHtml(matchItem); // If $sanitize is not present we pack the string in a $sce object for the ng-bind-html directive + } + return matchItem; + }; + }]); + +angular.module("uib/template/accordion/accordion-group.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("uib/template/accordion/accordion-group.html", + "
\n" + + "
\n" + "

\n" + - " {{heading}}\n" + + " {{heading}}\n" + "

\n" + "
\n" + - "
\n" + - "
\n" + + "
\n" + + "
\n" + "
\n" + - "
"); + "
\n" + + ""); }]); -angular.module("template/accordion/accordion.html", []).run(["$templateCache", function($templateCache) { - $templateCache.put("template/accordion/accordion.html", - "
"); +angular.module("uib/template/accordion/accordion.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("uib/template/accordion/accordion.html", + "
"); }]); -angular.module("template/alert/alert.html", []).run(["$templateCache", function($templateCache) { - $templateCache.put("template/alert/alert.html", - "
\n" + - " \n" + @@ -3830,59 +6931,73 @@ angular.module("template/alert/alert.html", []).run(["$templateCache", function( ""); }]); -angular.module("template/carousel/carousel.html", []).run(["$templateCache", function($templateCache) { - $templateCache.put("template/carousel/carousel.html", +angular.module("uib/template/carousel/carousel.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("uib/template/carousel/carousel.html", "
\n" + - "
    1\">\n" + - "
  1. \n" + - "
\n" + - "
\n" + - " 1\">\n" + - " 1\">\n" + + "
\n" + + " 1\">\n" + + " \n" + + " previous\n" + + " \n" + + " 1\">\n" + + " \n" + + " next\n" + + " \n" + + "
    1\">\n" + + "
  1. \n" + + " slide {{ $index + 1 }} of {{ slides.length }}, currently active\n" + + "
  2. \n" + + "
\n" + "
\n" + ""); }]); -angular.module("template/carousel/slide.html", []).run(["$templateCache", function($templateCache) { - $templateCache.put("template/carousel/slide.html", +angular.module("uib/template/carousel/slide.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("uib/template/carousel/slide.html", "
\n" + ""); }]); -angular.module("template/datepicker/datepicker.html", []).run(["$templateCache", function($templateCache) { - $templateCache.put("template/datepicker/datepicker.html", - "
\n" + - " \n" + - " \n" + - " \n" + - "
"); +angular.module("uib/template/datepicker/datepicker.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("uib/template/datepicker/datepicker.html", + "
\n" + + " \n" + + " \n" + + " \n" + + "
\n" + + ""); }]); -angular.module("template/datepicker/day.html", []).run(["$templateCache", function($templateCache) { - $templateCache.put("template/datepicker/day.html", - "\n" + +angular.module("uib/template/datepicker/day.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("uib/template/datepicker/day.html", + "
\n" + " \n" + " \n" + - " \n" + - " \n" + - " \n" + + " \n" + + " \n" + + " \n" + " \n" + " \n" + - " \n" + - " \n" + + " \n" + + " \n" + " \n" + " \n" + " \n" + - " \n" + - " \n" + - " \n" + + " \n" + + " \n" + " \n" + " \n" + @@ -3890,20 +7005,29 @@ angular.module("template/datepicker/day.html", []).run(["$templateCache", functi ""); }]); -angular.module("template/datepicker/month.html", []).run(["$templateCache", function($templateCache) { - $templateCache.put("template/datepicker/month.html", - "
{{label.abbr}}{{::label.abbr}}
{{ weekNumbers[$index] }}\n" + - " \n" + + "
{{ weekNumbers[$index] }}\n" + + " \n" + "
\n" + +angular.module("uib/template/datepicker/month.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("uib/template/datepicker/month.html", + "
\n" + " \n" + " \n" + - " \n" + - " \n" + - " \n" + + " \n" + + " \n" + + " \n" + " \n" + " \n" + " \n" + - " \n" + - " \n" + + " \n" + " \n" + " \n" + @@ -3911,35 +7035,29 @@ angular.module("template/datepicker/month.html", []).run(["$templateCache", func ""); }]); -angular.module("template/datepicker/popup.html", []).run(["$templateCache", function($templateCache) { - $templateCache.put("template/datepicker/popup.html", - "
    \n" + - "
  • \n" + - "
  • \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - "
  • \n" + - "
\n" + - ""); -}]); - -angular.module("template/datepicker/year.html", []).run(["$templateCache", function($templateCache) { - $templateCache.put("template/datepicker/year.html", - "
\n" + - " \n" + + "
\n" + + " \n" + "
\n" + +angular.module("uib/template/datepicker/year.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("uib/template/datepicker/year.html", + "
\n" + " \n" + " \n" + - " \n" + - " \n" + - " \n" + + " \n" + + " \n" + + " \n" + " \n" + " \n" + " \n" + - " \n" + - " \n" + + " \n" + " \n" + " \n" + @@ -3947,170 +7065,265 @@ angular.module("template/datepicker/year.html", []).run(["$templateCache", funct ""); }]); -angular.module("template/modal/backdrop.html", []).run(["$templateCache", function($templateCache) { - $templateCache.put("template/modal/backdrop.html", - "
\n" + + "
    \n" + + "
  • \n" + + "
  • \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + "
  • \n" + + "
\n" + + "
\n" + + ""); +}]); + +angular.module("uib/template/modal/backdrop.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("uib/template/modal/backdrop.html", + "
\n" + ""); }]); -angular.module("template/modal/window.html", []).run(["$templateCache", function($templateCache) { - $templateCache.put("template/modal/window.html", - "
\n" + - "
\n" + - "
"); -}]); - -angular.module("template/pagination/pager.html", []).run(["$templateCache", function($templateCache) { - $templateCache.put("template/pagination/pager.html", - ""); -}]); - -angular.module("template/pagination/pagination.html", []).run(["$templateCache", function($templateCache) { - $templateCache.put("template/pagination/pagination.html", - ""); -}]); - -angular.module("template/tooltip/tooltip-html-unsafe-popup.html", []).run(["$templateCache", function($templateCache) { - $templateCache.put("template/tooltip/tooltip-html-unsafe-popup.html", - "
\n" + - "
\n" + - "
\n" + +angular.module("uib/template/modal/window.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("uib/template/modal/window.html", + "
\n" + + "
\n" + "
\n" + ""); }]); -angular.module("template/tooltip/tooltip-popup.html", []).run(["$templateCache", function($templateCache) { - $templateCache.put("template/tooltip/tooltip-popup.html", - "
\n" + +angular.module("uib/template/pager/pager.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("uib/template/pager/pager.html", + "\n" + + ""); +}]); + +angular.module("uib/template/pagination/pagination.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("uib/template/pagination/pagination.html", + "\n" + + ""); +}]); + +angular.module("uib/template/tooltip/tooltip-html-popup.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("uib/template/tooltip/tooltip-html-popup.html", + "
\n" + + "
\n" + + "
\n" + + "
\n" + + ""); +}]); + +angular.module("uib/template/tooltip/tooltip-popup.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("uib/template/tooltip/tooltip-popup.html", + "
\n" + "
\n" + "
\n" + "
\n" + ""); }]); -angular.module("template/popover/popover.html", []).run(["$templateCache", function($templateCache) { - $templateCache.put("template/popover/popover.html", - "
\n" + +angular.module("uib/template/tooltip/tooltip-template-popup.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("uib/template/tooltip/tooltip-template-popup.html", + "
\n" + + "
\n" + + "
\n" + + "
\n" + + ""); +}]); + +angular.module("uib/template/popover/popover-html.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("uib/template/popover/popover-html.html", + "
\n" + "
\n" + "\n" + "
\n" + - "

\n" + + "

\n" + + "
\n" + + "
\n" + + "
\n" + + ""); +}]); + +angular.module("uib/template/popover/popover-template.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("uib/template/popover/popover-template.html", + "
\n" + + "
\n" + + "\n" + + "
\n" + + "

\n" + + "
\n" + + "
\n" + + "
\n" + + ""); +}]); + +angular.module("uib/template/popover/popover.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("uib/template/popover/popover.html", + "
\n" + + "
\n" + + "\n" + + "
\n" + + "

\n" + "
\n" + "
\n" + "
\n" + ""); }]); -angular.module("template/progressbar/bar.html", []).run(["$templateCache", function($templateCache) { - $templateCache.put("template/progressbar/bar.html", - "
"); +angular.module("uib/template/progressbar/bar.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("uib/template/progressbar/bar.html", + "
\n" + + ""); }]); -angular.module("template/progressbar/progress.html", []).run(["$templateCache", function($templateCache) { - $templateCache.put("template/progressbar/progress.html", - "
"); +angular.module("uib/template/progressbar/progress.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("uib/template/progressbar/progress.html", + "
"); }]); -angular.module("template/progressbar/progressbar.html", []).run(["$templateCache", function($templateCache) { - $templateCache.put("template/progressbar/progressbar.html", +angular.module("uib/template/progressbar/progressbar.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("uib/template/progressbar/progressbar.html", "
\n" + - "
\n" + - "
"); + "
\n" + + "
\n" + + ""); }]); -angular.module("template/rating/rating.html", []).run(["$templateCache", function($templateCache) { - $templateCache.put("template/rating/rating.html", - "\n" + - " \n" + - " ({{ $index < value ? '*' : ' ' }})\n" + - " \n" + - ""); +angular.module("uib/template/rating/rating.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("uib/template/rating/rating.html", + "\n" + + " ({{ $index < value ? '*' : ' ' }})\n" + + " \n" + + "\n" + + ""); }]); -angular.module("template/tabs/tab.html", []).run(["$templateCache", function($templateCache) { - $templateCache.put("template/tabs/tab.html", - "
  • \n" + - " {{heading}}\n" + +angular.module("uib/template/tabs/tab.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("uib/template/tabs/tab.html", + "
  • \n" + + " {{heading}}\n" + "
  • \n" + ""); }]); -angular.module("template/tabs/tabset-titles.html", []).run(["$templateCache", function($templateCache) { - $templateCache.put("template/tabs/tabset-titles.html", - "
      \n" + - "
    \n" + - ""); -}]); - -angular.module("template/tabs/tabset.html", []).run(["$templateCache", function($templateCache) { - $templateCache.put("template/tabs/tabset.html", - "\n" + +angular.module("uib/template/tabs/tabset.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("uib/template/tabs/tabset.html", "
    \n" + - "
      \n" + + "
        \n" + "
        \n" + - "
        \n" + + "
        \n" + "
        \n" + "
        \n" + "
        \n" + ""); }]); -angular.module("template/timepicker/timepicker.html", []).run(["$templateCache", function($templateCache) { - $templateCache.put("template/timepicker/timepicker.html", - "
        \n" + - " \n" + + "
        \n" + + " \n" + "
        \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + +angular.module("uib/template/timepicker/timepicker.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("uib/template/timepicker/timepicker.html", + "
         
        \n" + - " \n" + - " :\n" + - " \n" + - "
         
        \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + "
          
        \n" + + " \n" + + " :\n" + + " \n" + + " :\n" + + " \n" + + "
          
        \n" + ""); }]); -angular.module("template/typeahead/typeahead-match.html", []).run(["$templateCache", function($templateCache) { - $templateCache.put("template/typeahead/typeahead-match.html", - ""); +angular.module("uib/template/typeahead/typeahead-match.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("uib/template/typeahead/typeahead-match.html", + "\n" + + ""); }]); -angular.module("template/typeahead/typeahead-popup.html", []).run(["$templateCache", function($templateCache) { - $templateCache.put("template/typeahead/typeahead-popup.html", - "
          \n" + - "
        • \n" + - "
          \n" + +angular.module("uib/template/typeahead/typeahead-popup.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("uib/template/typeahead/typeahead-popup.html", + "
            \n" + + "
          • \n" + + "
            \n" + "
          • \n" + - "
          "); + "
        \n" + + ""); }]); +angular.module('ui.bootstrap.carousel').run(function() {!angular.$$csp().noInlineStyle && !angular.$$uibCarouselCss && angular.element(document).find('head').prepend(''); angular.$$uibCarouselCss = true; }); +angular.module('ui.bootstrap.datepicker').run(function() {!angular.$$csp().noInlineStyle && !angular.$$uibDatepickerCss && angular.element(document).find('head').prepend(''); angular.$$uibDatepickerCss = true; }); +angular.module('ui.bootstrap.position').run(function() {!angular.$$csp().noInlineStyle && !angular.$$uibPositionCss && angular.element(document).find('head').prepend(''); angular.$$uibPositionCss = true; }); +angular.module('ui.bootstrap.datepickerPopup').run(function() {!angular.$$csp().noInlineStyle && !angular.$$uibDatepickerpopupCss && angular.element(document).find('head').prepend(''); angular.$$uibDatepickerpopupCss = true; }); +angular.module('ui.bootstrap.tooltip').run(function() {!angular.$$csp().noInlineStyle && !angular.$$uibTooltipCss && angular.element(document).find('head').prepend(''); angular.$$uibTooltipCss = true; }); +angular.module('ui.bootstrap.timepicker').run(function() {!angular.$$csp().noInlineStyle && !angular.$$uibTimepickerCss && angular.element(document).find('head').prepend(''); angular.$$uibTimepickerCss = true; }); +angular.module('ui.bootstrap.typeahead').run(function() {!angular.$$csp().noInlineStyle && !angular.$$uibTypeaheadCss && angular.element(document).find('head').prepend(''); angular.$$uibTypeaheadCss = true; }); \ No newline at end of file diff --git a/app/bower_components/angular-bootstrap/ui-bootstrap-tpls.min.js b/app/bower_components/angular-bootstrap/ui-bootstrap-tpls.min.js index fa6a861..43f8f54 100644 --- a/app/bower_components/angular-bootstrap/ui-bootstrap-tpls.min.js +++ b/app/bower_components/angular-bootstrap/ui-bootstrap-tpls.min.js @@ -2,9 +2,9 @@ * angular-ui-bootstrap * http://angular-ui.github.io/bootstrap/ - * Version: 0.11.0 - 2014-05-01 + * Version: 1.3.2 - 2016-04-14 * License: MIT - */ -angular.module("ui.bootstrap",["ui.bootstrap.tpls","ui.bootstrap.transition","ui.bootstrap.collapse","ui.bootstrap.accordion","ui.bootstrap.alert","ui.bootstrap.bindHtml","ui.bootstrap.buttons","ui.bootstrap.carousel","ui.bootstrap.dateparser","ui.bootstrap.position","ui.bootstrap.datepicker","ui.bootstrap.dropdown","ui.bootstrap.modal","ui.bootstrap.pagination","ui.bootstrap.tooltip","ui.bootstrap.popover","ui.bootstrap.progressbar","ui.bootstrap.rating","ui.bootstrap.tabs","ui.bootstrap.timepicker","ui.bootstrap.typeahead"]),angular.module("ui.bootstrap.tpls",["template/accordion/accordion-group.html","template/accordion/accordion.html","template/alert/alert.html","template/carousel/carousel.html","template/carousel/slide.html","template/datepicker/datepicker.html","template/datepicker/day.html","template/datepicker/month.html","template/datepicker/popup.html","template/datepicker/year.html","template/modal/backdrop.html","template/modal/window.html","template/pagination/pager.html","template/pagination/pagination.html","template/tooltip/tooltip-html-unsafe-popup.html","template/tooltip/tooltip-popup.html","template/popover/popover.html","template/progressbar/bar.html","template/progressbar/progress.html","template/progressbar/progressbar.html","template/rating/rating.html","template/tabs/tab.html","template/tabs/tabset.html","template/timepicker/timepicker.html","template/typeahead/typeahead-match.html","template/typeahead/typeahead-popup.html"]),angular.module("ui.bootstrap.transition",[]).factory("$transition",["$q","$timeout","$rootScope",function(a,b,c){function d(a){for(var b in a)if(void 0!==f.style[b])return a[b]}var e=function(d,f,g){g=g||{};var h=a.defer(),i=e[g.animation?"animationEndEventName":"transitionEndEventName"],j=function(){c.$apply(function(){d.unbind(i,j),h.resolve(d)})};return i&&d.bind(i,j),b(function(){angular.isString(f)?d.addClass(f):angular.isFunction(f)?f(d):angular.isObject(f)&&d.css(f),i||h.resolve(d)}),h.promise.cancel=function(){i&&d.unbind(i,j),h.reject("Transition cancelled")},h.promise},f=document.createElement("trans"),g={WebkitTransition:"webkitTransitionEnd",MozTransition:"transitionend",OTransition:"oTransitionEnd",transition:"transitionend"},h={WebkitTransition:"webkitAnimationEnd",MozTransition:"animationend",OTransition:"oAnimationEnd",transition:"animationend"};return e.transitionEndEventName=d(g),e.animationEndEventName=d(h),e}]),angular.module("ui.bootstrap.collapse",["ui.bootstrap.transition"]).directive("collapse",["$transition",function(a){return{link:function(b,c,d){function e(b){function d(){j===e&&(j=void 0)}var e=a(c,b);return j&&j.cancel(),j=e,e.then(d,d),e}function f(){k?(k=!1,g()):(c.removeClass("collapse").addClass("collapsing"),e({height:c[0].scrollHeight+"px"}).then(g))}function g(){c.removeClass("collapsing"),c.addClass("collapse in"),c.css({height:"auto"})}function h(){if(k)k=!1,i(),c.css({height:0});else{c.css({height:c[0].scrollHeight+"px"});{c[0].offsetWidth}c.removeClass("collapse in").addClass("collapsing"),e({height:0}).then(i)}}function i(){c.removeClass("collapsing"),c.addClass("collapse")}var j,k=!0;b.$watch(d.collapse,function(a){a?h():f()})}}}]),angular.module("ui.bootstrap.accordion",["ui.bootstrap.collapse"]).constant("accordionConfig",{closeOthers:!0}).controller("AccordionController",["$scope","$attrs","accordionConfig",function(a,b,c){this.groups=[],this.closeOthers=function(d){var e=angular.isDefined(b.closeOthers)?a.$eval(b.closeOthers):c.closeOthers;e&&angular.forEach(this.groups,function(a){a!==d&&(a.isOpen=!1)})},this.addGroup=function(a){var b=this;this.groups.push(a),a.$on("$destroy",function(){b.removeGroup(a)})},this.removeGroup=function(a){var b=this.groups.indexOf(a);-1!==b&&this.groups.splice(b,1)}}]).directive("accordion",function(){return{restrict:"EA",controller:"AccordionController",transclude:!0,replace:!1,templateUrl:"template/accordion/accordion.html"}}).directive("accordionGroup",function(){return{require:"^accordion",restrict:"EA",transclude:!0,replace:!0,templateUrl:"template/accordion/accordion-group.html",scope:{heading:"@",isOpen:"=?",isDisabled:"=?"},controller:function(){this.setHeading=function(a){this.heading=a}},link:function(a,b,c,d){d.addGroup(a),a.$watch("isOpen",function(b){b&&d.closeOthers(a)}),a.toggleOpen=function(){a.isDisabled||(a.isOpen=!a.isOpen)}}}}).directive("accordionHeading",function(){return{restrict:"EA",transclude:!0,template:"",replace:!0,require:"^accordionGroup",link:function(a,b,c,d,e){d.setHeading(e(a,function(){}))}}}).directive("accordionTransclude",function(){return{require:"^accordionGroup",link:function(a,b,c,d){a.$watch(function(){return d[c.accordionTransclude]},function(a){a&&(b.html(""),b.append(a))})}}}),angular.module("ui.bootstrap.alert",[]).controller("AlertController",["$scope","$attrs",function(a,b){a.closeable="close"in b}]).directive("alert",function(){return{restrict:"EA",controller:"AlertController",templateUrl:"template/alert/alert.html",transclude:!0,replace:!0,scope:{type:"@",close:"&"}}}),angular.module("ui.bootstrap.bindHtml",[]).directive("bindHtmlUnsafe",function(){return function(a,b,c){b.addClass("ng-binding").data("$binding",c.bindHtmlUnsafe),a.$watch(c.bindHtmlUnsafe,function(a){b.html(a||"")})}}),angular.module("ui.bootstrap.buttons",[]).constant("buttonConfig",{activeClass:"active",toggleEvent:"click"}).controller("ButtonsController",["buttonConfig",function(a){this.activeClass=a.activeClass||"active",this.toggleEvent=a.toggleEvent||"click"}]).directive("btnRadio",function(){return{require:["btnRadio","ngModel"],controller:"ButtonsController",link:function(a,b,c,d){var e=d[0],f=d[1];f.$render=function(){b.toggleClass(e.activeClass,angular.equals(f.$modelValue,a.$eval(c.btnRadio)))},b.bind(e.toggleEvent,function(){var d=b.hasClass(e.activeClass);(!d||angular.isDefined(c.uncheckable))&&a.$apply(function(){f.$setViewValue(d?null:a.$eval(c.btnRadio)),f.$render()})})}}}).directive("btnCheckbox",function(){return{require:["btnCheckbox","ngModel"],controller:"ButtonsController",link:function(a,b,c,d){function e(){return g(c.btnCheckboxTrue,!0)}function f(){return g(c.btnCheckboxFalse,!1)}function g(b,c){var d=a.$eval(b);return angular.isDefined(d)?d:c}var h=d[0],i=d[1];i.$render=function(){b.toggleClass(h.activeClass,angular.equals(i.$modelValue,e()))},b.bind(h.toggleEvent,function(){a.$apply(function(){i.$setViewValue(b.hasClass(h.activeClass)?f():e()),i.$render()})})}}}),angular.module("ui.bootstrap.carousel",["ui.bootstrap.transition"]).controller("CarouselController",["$scope","$timeout","$transition",function(a,b,c){function d(){e();var c=+a.interval;!isNaN(c)&&c>=0&&(g=b(f,c))}function e(){g&&(b.cancel(g),g=null)}function f(){h?(a.next(),d()):a.pause()}var g,h,i=this,j=i.slides=a.slides=[],k=-1;i.currentSlide=null;var l=!1;i.select=a.select=function(e,f){function g(){if(!l){if(i.currentSlide&&angular.isString(f)&&!a.noTransition&&e.$element){e.$element.addClass(f);{e.$element[0].offsetWidth}angular.forEach(j,function(a){angular.extend(a,{direction:"",entering:!1,leaving:!1,active:!1})}),angular.extend(e,{direction:f,active:!0,entering:!0}),angular.extend(i.currentSlide||{},{direction:f,leaving:!0}),a.$currentTransition=c(e.$element,{}),function(b,c){a.$currentTransition.then(function(){h(b,c)},function(){h(b,c)})}(e,i.currentSlide)}else h(e,i.currentSlide);i.currentSlide=e,k=m,d()}}function h(b,c){angular.extend(b,{direction:"",active:!0,leaving:!1,entering:!1}),angular.extend(c||{},{direction:"",active:!1,leaving:!1,entering:!1}),a.$currentTransition=null}var m=j.indexOf(e);void 0===f&&(f=m>k?"next":"prev"),e&&e!==i.currentSlide&&(a.$currentTransition?(a.$currentTransition.cancel(),b(g)):g())},a.$on("$destroy",function(){l=!0}),i.indexOfSlide=function(a){return j.indexOf(a)},a.next=function(){var b=(k+1)%j.length;return a.$currentTransition?void 0:i.select(j[b],"next")},a.prev=function(){var b=0>k-1?j.length-1:k-1;return a.$currentTransition?void 0:i.select(j[b],"prev")},a.isActive=function(a){return i.currentSlide===a},a.$watch("interval",d),a.$on("$destroy",e),a.play=function(){h||(h=!0,d())},a.pause=function(){a.noPause||(h=!1,e())},i.addSlide=function(b,c){b.$element=c,j.push(b),1===j.length||b.active?(i.select(j[j.length-1]),1==j.length&&a.play()):b.active=!1},i.removeSlide=function(a){var b=j.indexOf(a);j.splice(b,1),j.length>0&&a.active?i.select(b>=j.length?j[b-1]:j[b]):k>b&&k--}}]).directive("carousel",[function(){return{restrict:"EA",transclude:!0,replace:!0,controller:"CarouselController",require:"carousel",templateUrl:"template/carousel/carousel.html",scope:{interval:"=",noTransition:"=",noPause:"="}}}]).directive("slide",function(){return{require:"^carousel",restrict:"EA",transclude:!0,replace:!0,templateUrl:"template/carousel/slide.html",scope:{active:"=?"},link:function(a,b,c,d){d.addSlide(a,b),a.$on("$destroy",function(){d.removeSlide(a)}),a.$watch("active",function(b){b&&d.select(a)})}}}),angular.module("ui.bootstrap.dateparser",[]).service("dateParser",["$locale","orderByFilter",function(a,b){function c(a,b,c){return 1===b&&c>28?29===c&&(a%4===0&&a%100!==0||a%400===0):3===b||5===b||8===b||10===b?31>c:!0}this.parsers={};var d={yyyy:{regex:"\\d{4}",apply:function(a){this.year=+a}},yy:{regex:"\\d{2}",apply:function(a){this.year=+a+2e3}},y:{regex:"\\d{1,4}",apply:function(a){this.year=+a}},MMMM:{regex:a.DATETIME_FORMATS.MONTH.join("|"),apply:function(b){this.month=a.DATETIME_FORMATS.MONTH.indexOf(b)}},MMM:{regex:a.DATETIME_FORMATS.SHORTMONTH.join("|"),apply:function(b){this.month=a.DATETIME_FORMATS.SHORTMONTH.indexOf(b)}},MM:{regex:"0[1-9]|1[0-2]",apply:function(a){this.month=a-1}},M:{regex:"[1-9]|1[0-2]",apply:function(a){this.month=a-1}},dd:{regex:"[0-2][0-9]{1}|3[0-1]{1}",apply:function(a){this.date=+a}},d:{regex:"[1-2]?[0-9]{1}|3[0-1]{1}",apply:function(a){this.date=+a}},EEEE:{regex:a.DATETIME_FORMATS.DAY.join("|")},EEE:{regex:a.DATETIME_FORMATS.SHORTDAY.join("|")}};this.createParser=function(a){var c=[],e=a.split("");return angular.forEach(d,function(b,d){var f=a.indexOf(d);if(f>-1){a=a.split(""),e[f]="("+b.regex+")",a[f]="$";for(var g=f+1,h=f+d.length;h>g;g++)e[g]="",a[g]="$";a=a.join(""),c.push({index:f,apply:b.apply})}}),{regex:new RegExp("^"+e.join("")+"$"),map:b(c,"index")}},this.parse=function(b,d){if(!angular.isString(b))return b;d=a.DATETIME_FORMATS[d]||d,this.parsers[d]||(this.parsers[d]=this.createParser(d));var e=this.parsers[d],f=e.regex,g=e.map,h=b.match(f);if(h&&h.length){for(var i,j={year:1900,month:0,date:1,hours:0},k=1,l=h.length;l>k;k++){var m=g[k-1];m.apply&&m.apply.call(j,h[k])}return c(j.year,j.month,j.date)&&(i=new Date(j.year,j.month,j.date,j.hours)),i}}}]),angular.module("ui.bootstrap.position",[]).factory("$position",["$document","$window",function(a,b){function c(a,c){return a.currentStyle?a.currentStyle[c]:b.getComputedStyle?b.getComputedStyle(a)[c]:a.style[c]}function d(a){return"static"===(c(a,"position")||"static")}var e=function(b){for(var c=a[0],e=b.offsetParent||c;e&&e!==c&&d(e);)e=e.offsetParent;return e||c};return{position:function(b){var c=this.offset(b),d={top:0,left:0},f=e(b[0]);f!=a[0]&&(d=this.offset(angular.element(f)),d.top+=f.clientTop-f.scrollTop,d.left+=f.clientLeft-f.scrollLeft);var g=b[0].getBoundingClientRect();return{width:g.width||b.prop("offsetWidth"),height:g.height||b.prop("offsetHeight"),top:c.top-d.top,left:c.left-d.left}},offset:function(c){var d=c[0].getBoundingClientRect();return{width:d.width||c.prop("offsetWidth"),height:d.height||c.prop("offsetHeight"),top:d.top+(b.pageYOffset||a[0].documentElement.scrollTop),left:d.left+(b.pageXOffset||a[0].documentElement.scrollLeft)}},positionElements:function(a,b,c,d){var e,f,g,h,i=c.split("-"),j=i[0],k=i[1]||"center";e=d?this.offset(a):this.position(a),f=b.prop("offsetWidth"),g=b.prop("offsetHeight");var l={center:function(){return e.left+e.width/2-f/2},left:function(){return e.left},right:function(){return e.left+e.width}},m={center:function(){return e.top+e.height/2-g/2},top:function(){return e.top},bottom:function(){return e.top+e.height}};switch(j){case"right":h={top:m[k](),left:l[j]()};break;case"left":h={top:m[k](),left:e.left-f};break;case"bottom":h={top:m[j](),left:l[k]()};break;default:h={top:e.top-g,left:l[k]()}}return h}}}]),angular.module("ui.bootstrap.datepicker",["ui.bootstrap.dateparser","ui.bootstrap.position"]).constant("datepickerConfig",{formatDay:"dd",formatMonth:"MMMM",formatYear:"yyyy",formatDayHeader:"EEE",formatDayTitle:"MMMM yyyy",formatMonthTitle:"yyyy",datepickerMode:"day",minMode:"day",maxMode:"year",showWeeks:!0,startingDay:0,yearRange:20,minDate:null,maxDate:null}).controller("DatepickerController",["$scope","$attrs","$parse","$interpolate","$timeout","$log","dateFilter","datepickerConfig",function(a,b,c,d,e,f,g,h){var i=this,j={$setViewValue:angular.noop};this.modes=["day","month","year"],angular.forEach(["formatDay","formatMonth","formatYear","formatDayHeader","formatDayTitle","formatMonthTitle","minMode","maxMode","showWeeks","startingDay","yearRange"],function(c,e){i[c]=angular.isDefined(b[c])?8>e?d(b[c])(a.$parent):a.$parent.$eval(b[c]):h[c]}),angular.forEach(["minDate","maxDate"],function(d){b[d]?a.$parent.$watch(c(b[d]),function(a){i[d]=a?new Date(a):null,i.refreshView()}):i[d]=h[d]?new Date(h[d]):null}),a.datepickerMode=a.datepickerMode||h.datepickerMode,a.uniqueId="datepicker-"+a.$id+"-"+Math.floor(1e4*Math.random()),this.activeDate=angular.isDefined(b.initDate)?a.$parent.$eval(b.initDate):new Date,a.isActive=function(b){return 0===i.compare(b.date,i.activeDate)?(a.activeDateId=b.uid,!0):!1},this.init=function(a){j=a,j.$render=function(){i.render()}},this.render=function(){if(j.$modelValue){var a=new Date(j.$modelValue),b=!isNaN(a);b?this.activeDate=a:f.error('Datepicker directive: "ng-model" value must be a Date object, a number of milliseconds since 01.01.1970 or a string representing an RFC2822 or ISO 8601 date.'),j.$setValidity("date",b)}this.refreshView()},this.refreshView=function(){if(this.element){this._refreshView();var a=j.$modelValue?new Date(j.$modelValue):null;j.$setValidity("date-disabled",!a||this.element&&!this.isDisabled(a))}},this.createDateObject=function(a,b){var c=j.$modelValue?new Date(j.$modelValue):null;return{date:a,label:g(a,b),selected:c&&0===this.compare(a,c),disabled:this.isDisabled(a),current:0===this.compare(a,new Date)}},this.isDisabled=function(c){return this.minDate&&this.compare(c,this.minDate)<0||this.maxDate&&this.compare(c,this.maxDate)>0||b.dateDisabled&&a.dateDisabled({date:c,mode:a.datepickerMode})},this.split=function(a,b){for(var c=[];a.length>0;)c.push(a.splice(0,b));return c},a.select=function(b){if(a.datepickerMode===i.minMode){var c=j.$modelValue?new Date(j.$modelValue):new Date(0,0,0,0,0,0,0);c.setFullYear(b.getFullYear(),b.getMonth(),b.getDate()),j.$setViewValue(c),j.$render()}else i.activeDate=b,a.datepickerMode=i.modes[i.modes.indexOf(a.datepickerMode)-1]},a.move=function(a){var b=i.activeDate.getFullYear()+a*(i.step.years||0),c=i.activeDate.getMonth()+a*(i.step.months||0);i.activeDate.setFullYear(b,c,1),i.refreshView()},a.toggleMode=function(b){b=b||1,a.datepickerMode===i.maxMode&&1===b||a.datepickerMode===i.minMode&&-1===b||(a.datepickerMode=i.modes[i.modes.indexOf(a.datepickerMode)+b])},a.keys={13:"enter",32:"space",33:"pageup",34:"pagedown",35:"end",36:"home",37:"left",38:"up",39:"right",40:"down"};var k=function(){e(function(){i.element[0].focus()},0,!1)};a.$on("datepicker.focus",k),a.keydown=function(b){var c=a.keys[b.which];if(c&&!b.shiftKey&&!b.altKey)if(b.preventDefault(),b.stopPropagation(),"enter"===c||"space"===c){if(i.isDisabled(i.activeDate))return;a.select(i.activeDate),k()}else!b.ctrlKey||"up"!==c&&"down"!==c?(i.handleKeyDown(c,b),i.refreshView()):(a.toggleMode("up"===c?1:-1),k())}}]).directive("datepicker",function(){return{restrict:"EA",replace:!0,templateUrl:"template/datepicker/datepicker.html",scope:{datepickerMode:"=?",dateDisabled:"&"},require:["datepicker","?^ngModel"],controller:"DatepickerController",link:function(a,b,c,d){var e=d[0],f=d[1];f&&e.init(f)}}}).directive("daypicker",["dateFilter",function(a){return{restrict:"EA",replace:!0,templateUrl:"template/datepicker/day.html",require:"^datepicker",link:function(b,c,d,e){function f(a,b){return 1!==b||a%4!==0||a%100===0&&a%400!==0?i[b]:29}function g(a,b){var c=new Array(b),d=new Date(a),e=0;for(d.setHours(12);b>e;)c[e++]=new Date(d),d.setDate(d.getDate()+1);return c}function h(a){var b=new Date(a);b.setDate(b.getDate()+4-(b.getDay()||7));var c=b.getTime();return b.setMonth(0),b.setDate(1),Math.floor(Math.round((c-b)/864e5)/7)+1}b.showWeeks=e.showWeeks,e.step={months:1},e.element=c;var i=[31,28,31,30,31,30,31,31,30,31,30,31];e._refreshView=function(){var c=e.activeDate.getFullYear(),d=e.activeDate.getMonth(),f=new Date(c,d,1),i=e.startingDay-f.getDay(),j=i>0?7-i:-i,k=new Date(f);j>0&&k.setDate(-j+1);for(var l=g(k,42),m=0;42>m;m++)l[m]=angular.extend(e.createDateObject(l[m],e.formatDay),{secondary:l[m].getMonth()!==d,uid:b.uniqueId+"-"+m});b.labels=new Array(7);for(var n=0;7>n;n++)b.labels[n]={abbr:a(l[n].date,e.formatDayHeader),full:a(l[n].date,"EEEE")};if(b.title=a(e.activeDate,e.formatDayTitle),b.rows=e.split(l,7),b.showWeeks){b.weekNumbers=[];for(var o=h(b.rows[0][0].date),p=b.rows.length;b.weekNumbers.push(o++)f;f++)c[f]=angular.extend(e.createDateObject(new Date(d,f,1),e.formatMonth),{uid:b.uniqueId+"-"+f});b.title=a(e.activeDate,e.formatMonthTitle),b.rows=e.split(c,3)},e.compare=function(a,b){return new Date(a.getFullYear(),a.getMonth())-new Date(b.getFullYear(),b.getMonth())},e.handleKeyDown=function(a){var b=e.activeDate.getMonth();if("left"===a)b-=1;else if("up"===a)b-=3;else if("right"===a)b+=1;else if("down"===a)b+=3;else if("pageup"===a||"pagedown"===a){var c=e.activeDate.getFullYear()+("pageup"===a?-1:1);e.activeDate.setFullYear(c)}else"home"===a?b=0:"end"===a&&(b=11);e.activeDate.setMonth(b)},e.refreshView()}}}]).directive("yearpicker",["dateFilter",function(){return{restrict:"EA",replace:!0,templateUrl:"template/datepicker/year.html",require:"^datepicker",link:function(a,b,c,d){function e(a){return parseInt((a-1)/f,10)*f+1}var f=d.yearRange;d.step={years:f},d.element=b,d._refreshView=function(){for(var b=new Array(f),c=0,g=e(d.activeDate.getFullYear());f>c;c++)b[c]=angular.extend(d.createDateObject(new Date(g+c,0,1),d.formatYear),{uid:a.uniqueId+"-"+c});a.title=[b[0].label,b[f-1].label].join(" - "),a.rows=d.split(b,5)},d.compare=function(a,b){return a.getFullYear()-b.getFullYear()},d.handleKeyDown=function(a){var b=d.activeDate.getFullYear();"left"===a?b-=1:"up"===a?b-=5:"right"===a?b+=1:"down"===a?b+=5:"pageup"===a||"pagedown"===a?b+=("pageup"===a?-1:1)*d.step.years:"home"===a?b=e(d.activeDate.getFullYear()):"end"===a&&(b=e(d.activeDate.getFullYear())+f-1),d.activeDate.setFullYear(b)},d.refreshView()}}}]).constant("datepickerPopupConfig",{datepickerPopup:"yyyy-MM-dd",currentText:"Today",clearText:"Clear",closeText:"Done",closeOnDateSelection:!0,appendToBody:!1,showButtonBar:!0}).directive("datepickerPopup",["$compile","$parse","$document","$position","dateFilter","dateParser","datepickerPopupConfig",function(a,b,c,d,e,f,g){return{restrict:"EA",require:"ngModel",scope:{isOpen:"=?",currentText:"@",clearText:"@",closeText:"@",dateDisabled:"&"},link:function(h,i,j,k){function l(a){return a.replace(/([A-Z])/g,function(a){return"-"+a.toLowerCase()})}function m(a){if(a){if(angular.isDate(a)&&!isNaN(a))return k.$setValidity("date",!0),a;if(angular.isString(a)){var b=f.parse(a,n)||new Date(a);return isNaN(b)?void k.$setValidity("date",!1):(k.$setValidity("date",!0),b)}return void k.$setValidity("date",!1)}return k.$setValidity("date",!0),null}var n,o=angular.isDefined(j.closeOnDateSelection)?h.$parent.$eval(j.closeOnDateSelection):g.closeOnDateSelection,p=angular.isDefined(j.datepickerAppendToBody)?h.$parent.$eval(j.datepickerAppendToBody):g.appendToBody;h.showButtonBar=angular.isDefined(j.showButtonBar)?h.$parent.$eval(j.showButtonBar):g.showButtonBar,h.getText=function(a){return h[a+"Text"]||g[a+"Text"]},j.$observe("datepickerPopup",function(a){n=a||g.datepickerPopup,k.$render()});var q=angular.element("
        ");q.attr({"ng-model":"date","ng-change":"dateSelection()"});var r=angular.element(q.children()[0]);j.datepickerOptions&&angular.forEach(h.$parent.$eval(j.datepickerOptions),function(a,b){r.attr(l(b),a)}),angular.forEach(["minDate","maxDate"],function(a){j[a]&&(h.$parent.$watch(b(j[a]),function(b){h[a]=b}),r.attr(l(a),a))}),j.dateDisabled&&r.attr("date-disabled","dateDisabled({ date: date, mode: mode })"),k.$parsers.unshift(m),h.dateSelection=function(a){angular.isDefined(a)&&(h.date=a),k.$setViewValue(h.date),k.$render(),o&&(h.isOpen=!1,i[0].focus())},i.bind("input change keyup",function(){h.$apply(function(){h.date=k.$modelValue})}),k.$render=function(){var a=k.$viewValue?e(k.$viewValue,n):"";i.val(a),h.date=m(k.$modelValue)};var s=function(a){h.isOpen&&a.target!==i[0]&&h.$apply(function(){h.isOpen=!1})},t=function(a){h.keydown(a)};i.bind("keydown",t),h.keydown=function(a){27===a.which?(a.preventDefault(),a.stopPropagation(),h.close()):40!==a.which||h.isOpen||(h.isOpen=!0)},h.$watch("isOpen",function(a){a?(h.$broadcast("datepicker.focus"),h.position=p?d.offset(i):d.position(i),h.position.top=h.position.top+i.prop("offsetHeight"),c.bind("click",s)):c.unbind("click",s)}),h.select=function(a){if("today"===a){var b=new Date;angular.isDate(k.$modelValue)?(a=new Date(k.$modelValue),a.setFullYear(b.getFullYear(),b.getMonth(),b.getDate())):a=new Date(b.setHours(0,0,0,0))}h.dateSelection(a)},h.close=function(){h.isOpen=!1,i[0].focus()};var u=a(q)(h);p?c.find("body").append(u):i.after(u),h.$on("$destroy",function(){u.remove(),i.unbind("keydown",t),c.unbind("click",s)})}}}]).directive("datepickerPopupWrap",function(){return{restrict:"EA",replace:!0,transclude:!0,templateUrl:"template/datepicker/popup.html",link:function(a,b){b.bind("click",function(a){a.preventDefault(),a.stopPropagation()})}}}),angular.module("ui.bootstrap.dropdown",[]).constant("dropdownConfig",{openClass:"open"}).service("dropdownService",["$document",function(a){var b=null;this.open=function(e){b||(a.bind("click",c),a.bind("keydown",d)),b&&b!==e&&(b.isOpen=!1),b=e},this.close=function(e){b===e&&(b=null,a.unbind("click",c),a.unbind("keydown",d))};var c=function(a){a&&a.isDefaultPrevented()||b.$apply(function(){b.isOpen=!1})},d=function(a){27===a.which&&(b.focusToggleElement(),c())}}]).controller("DropdownController",["$scope","$attrs","$parse","dropdownConfig","dropdownService","$animate",function(a,b,c,d,e,f){var g,h=this,i=a.$new(),j=d.openClass,k=angular.noop,l=b.onToggle?c(b.onToggle):angular.noop;this.init=function(d){h.$element=d,b.isOpen&&(g=c(b.isOpen),k=g.assign,a.$watch(g,function(a){i.isOpen=!!a}))},this.toggle=function(a){return i.isOpen=arguments.length?!!a:!i.isOpen},this.isOpen=function(){return i.isOpen},i.focusToggleElement=function(){h.toggleElement&&h.toggleElement[0].focus()},i.$watch("isOpen",function(b,c){f[b?"addClass":"removeClass"](h.$element,j),b?(i.focusToggleElement(),e.open(i)):e.close(i),k(a,b),angular.isDefined(b)&&b!==c&&l(a,{open:!!b})}),a.$on("$locationChangeSuccess",function(){i.isOpen=!1}),a.$on("$destroy",function(){i.$destroy()})}]).directive("dropdown",function(){return{restrict:"CA",controller:"DropdownController",link:function(a,b,c,d){d.init(b)}}}).directive("dropdownToggle",function(){return{restrict:"CA",require:"?^dropdown",link:function(a,b,c,d){if(d){d.toggleElement=b;var e=function(e){e.preventDefault(),b.hasClass("disabled")||c.disabled||a.$apply(function(){d.toggle()})};b.bind("click",e),b.attr({"aria-haspopup":!0,"aria-expanded":!1}),a.$watch(d.isOpen,function(a){b.attr("aria-expanded",!!a)}),a.$on("$destroy",function(){b.unbind("click",e)})}}}}),angular.module("ui.bootstrap.modal",["ui.bootstrap.transition"]).factory("$$stackedMap",function(){return{createNew:function(){var a=[];return{add:function(b,c){a.push({key:b,value:c})},get:function(b){for(var c=0;c0),i()})}function i(){if(k&&-1==g()){var a=l;j(k,l,150,function(){a.$destroy(),a=null}),k=void 0,l=void 0}}function j(c,d,e,f){function g(){g.done||(g.done=!0,c.remove(),f&&f())}d.animate=!1;var h=a.transitionEndEventName;if(h){var i=b(g,e);c.bind(h,function(){b.cancel(i),g(),d.$apply()})}else b(g,0)}var k,l,m="modal-open",n=f.createNew(),o={};return e.$watch(g,function(a){l&&(l.index=a)}),c.bind("keydown",function(a){var b;27===a.which&&(b=n.top(),b&&b.value.keyboard&&(a.preventDefault(),e.$apply(function(){o.dismiss(b.key,"escape key press")})))}),o.open=function(a,b){n.add(a,{deferred:b.deferred,modalScope:b.scope,backdrop:b.backdrop,keyboard:b.keyboard});var f=c.find("body").eq(0),h=g();h>=0&&!k&&(l=e.$new(!0),l.index=h,k=d("
        ")(l),f.append(k));var i=angular.element("
        ");i.attr({"template-url":b.windowTemplateUrl,"window-class":b.windowClass,size:b.size,index:n.length()-1,animate:"animate"}).html(b.content);var j=d(i)(b.scope);n.top().value.modalDomEl=j,f.append(j),f.addClass(m)},o.close=function(a,b){var c=n.get(a).value;c&&(c.deferred.resolve(b),h(a))},o.dismiss=function(a,b){var c=n.get(a).value;c&&(c.deferred.reject(b),h(a))},o.dismissAll=function(a){for(var b=this.getTop();b;)this.dismiss(b.key,a),b=this.getTop()},o.getTop=function(){return n.top()},o}]).provider("$modal",function(){var a={options:{backdrop:!0,keyboard:!0},$get:["$injector","$rootScope","$q","$http","$templateCache","$controller","$modalStack",function(b,c,d,e,f,g,h){function i(a){return a.template?d.when(a.template):e.get(a.templateUrl,{cache:f}).then(function(a){return a.data})}function j(a){var c=[];return angular.forEach(a,function(a){(angular.isFunction(a)||angular.isArray(a))&&c.push(d.when(b.invoke(a)))}),c}var k={};return k.open=function(b){var e=d.defer(),f=d.defer(),k={result:e.promise,opened:f.promise,close:function(a){h.close(k,a)},dismiss:function(a){h.dismiss(k,a)}};if(b=angular.extend({},a.options,b),b.resolve=b.resolve||{},!b.template&&!b.templateUrl)throw new Error("One of template or templateUrl options is required.");var l=d.all([i(b)].concat(j(b.resolve)));return l.then(function(a){var d=(b.scope||c).$new();d.$close=k.close,d.$dismiss=k.dismiss;var f,i={},j=1;b.controller&&(i.$scope=d,i.$modalInstance=k,angular.forEach(b.resolve,function(b,c){i[c]=a[j++]}),f=g(b.controller,i)),h.open(k,{scope:d,deferred:e,content:a[0],backdrop:b.backdrop,keyboard:b.keyboard,windowClass:b.windowClass,windowTemplateUrl:b.windowTemplateUrl,size:b.size})},function(a){e.reject(a)}),l.then(function(){f.resolve(!0)},function(){f.reject(!1)}),k},k}]};return a}),angular.module("ui.bootstrap.pagination",[]).controller("PaginationController",["$scope","$attrs","$parse",function(a,b,c){var d=this,e={$setViewValue:angular.noop},f=b.numPages?c(b.numPages).assign:angular.noop;this.init=function(f,g){e=f,this.config=g,e.$render=function(){d.render()},b.itemsPerPage?a.$parent.$watch(c(b.itemsPerPage),function(b){d.itemsPerPage=parseInt(b,10),a.totalPages=d.calculateTotalPages()}):this.itemsPerPage=g.itemsPerPage},this.calculateTotalPages=function(){var b=this.itemsPerPage<1?1:Math.ceil(a.totalItems/this.itemsPerPage);return Math.max(b||0,1)},this.render=function(){a.page=parseInt(e.$viewValue,10)||1},a.selectPage=function(b){a.page!==b&&b>0&&b<=a.totalPages&&(e.$setViewValue(b),e.$render())},a.getText=function(b){return a[b+"Text"]||d.config[b+"Text"]},a.noPrevious=function(){return 1===a.page},a.noNext=function(){return a.page===a.totalPages},a.$watch("totalItems",function(){a.totalPages=d.calculateTotalPages()}),a.$watch("totalPages",function(b){f(a.$parent,b),a.page>b?a.selectPage(b):e.$render()})}]).constant("paginationConfig",{itemsPerPage:10,boundaryLinks:!1,directionLinks:!0,firstText:"First",previousText:"Previous",nextText:"Next",lastText:"Last",rotate:!0}).directive("pagination",["$parse","paginationConfig",function(a,b){return{restrict:"EA",scope:{totalItems:"=",firstText:"@",previousText:"@",nextText:"@",lastText:"@"},require:["pagination","?ngModel"],controller:"PaginationController",templateUrl:"template/pagination/pagination.html",replace:!0,link:function(c,d,e,f){function g(a,b,c){return{number:a,text:b,active:c}}function h(a,b){var c=[],d=1,e=b,f=angular.isDefined(k)&&b>k;f&&(l?(d=Math.max(a-Math.floor(k/2),1),e=d+k-1,e>b&&(e=b,d=e-k+1)):(d=(Math.ceil(a/k)-1)*k+1,e=Math.min(d+k-1,b)));for(var h=d;e>=h;h++){var i=g(h,h,h===a);c.push(i)}if(f&&!l){if(d>1){var j=g(d-1,"...",!1);c.unshift(j)}if(b>e){var m=g(e+1,"...",!1);c.push(m)}}return c}var i=f[0],j=f[1];if(j){var k=angular.isDefined(e.maxSize)?c.$parent.$eval(e.maxSize):b.maxSize,l=angular.isDefined(e.rotate)?c.$parent.$eval(e.rotate):b.rotate;c.boundaryLinks=angular.isDefined(e.boundaryLinks)?c.$parent.$eval(e.boundaryLinks):b.boundaryLinks,c.directionLinks=angular.isDefined(e.directionLinks)?c.$parent.$eval(e.directionLinks):b.directionLinks,i.init(j,b),e.maxSize&&c.$parent.$watch(a(e.maxSize),function(a){k=parseInt(a,10),i.render()});var m=i.render;i.render=function(){m(),c.page>0&&c.page<=c.totalPages&&(c.pages=h(c.page,c.totalPages))}}}}}]).constant("pagerConfig",{itemsPerPage:10,previousText:"« Previous",nextText:"Next »",align:!0}).directive("pager",["pagerConfig",function(a){return{restrict:"EA",scope:{totalItems:"=",previousText:"@",nextText:"@"},require:["pager","?ngModel"],controller:"PaginationController",templateUrl:"template/pagination/pager.html",replace:!0,link:function(b,c,d,e){var f=e[0],g=e[1];g&&(b.align=angular.isDefined(d.align)?b.$parent.$eval(d.align):a.align,f.init(g,a))}}}]),angular.module("ui.bootstrap.tooltip",["ui.bootstrap.position","ui.bootstrap.bindHtml"]).provider("$tooltip",function(){function a(a){var b=/[A-Z]/g,c="-"; -return a.replace(b,function(a,b){return(b?c:"")+a.toLowerCase()})}var b={placement:"top",animation:!0,popupDelay:0},c={mouseenter:"mouseleave",click:"click",focus:"blur"},d={};this.options=function(a){angular.extend(d,a)},this.setTriggers=function(a){angular.extend(c,a)},this.$get=["$window","$compile","$timeout","$parse","$document","$position","$interpolate",function(e,f,g,h,i,j,k){return function(e,l,m){function n(a){var b=a||o.trigger||m,d=c[b]||b;return{show:b,hide:d}}var o=angular.extend({},b,d),p=a(e),q=k.startSymbol(),r=k.endSymbol(),s="
        ';return{restrict:"EA",scope:!0,compile:function(){var a=f(s);return function(b,c,d){function f(){b.tt_isOpen?m():k()}function k(){(!y||b.$eval(d[l+"Enable"]))&&(b.tt_popupDelay?v||(v=g(p,b.tt_popupDelay,!1),v.then(function(a){a()})):p()())}function m(){b.$apply(function(){q()})}function p(){return v=null,u&&(g.cancel(u),u=null),b.tt_content?(r(),t.css({top:0,left:0,display:"block"}),w?i.find("body").append(t):c.after(t),z(),b.tt_isOpen=!0,b.$digest(),z):angular.noop}function q(){b.tt_isOpen=!1,g.cancel(v),v=null,b.tt_animation?u||(u=g(s,500)):s()}function r(){t&&s(),t=a(b,function(){}),b.$digest()}function s(){u=null,t&&(t.remove(),t=null)}var t,u,v,w=angular.isDefined(o.appendToBody)?o.appendToBody:!1,x=n(void 0),y=angular.isDefined(d[l+"Enable"]),z=function(){var a=j.positionElements(c,t,b.tt_placement,w);a.top+="px",a.left+="px",t.css(a)};b.tt_isOpen=!1,d.$observe(e,function(a){b.tt_content=a,!a&&b.tt_isOpen&&q()}),d.$observe(l+"Title",function(a){b.tt_title=a}),d.$observe(l+"Placement",function(a){b.tt_placement=angular.isDefined(a)?a:o.placement}),d.$observe(l+"PopupDelay",function(a){var c=parseInt(a,10);b.tt_popupDelay=isNaN(c)?o.popupDelay:c});var A=function(){c.unbind(x.show,k),c.unbind(x.hide,m)};d.$observe(l+"Trigger",function(a){A(),x=n(a),x.show===x.hide?c.bind(x.show,f):(c.bind(x.show,k),c.bind(x.hide,m))});var B=b.$eval(d[l+"Animation"]);b.tt_animation=angular.isDefined(B)?!!B:o.animation,d.$observe(l+"AppendToBody",function(a){w=angular.isDefined(a)?h(a)(b):w}),w&&b.$on("$locationChangeSuccess",function(){b.tt_isOpen&&q()}),b.$on("$destroy",function(){g.cancel(u),g.cancel(v),A(),s()})}}}}}]}).directive("tooltipPopup",function(){return{restrict:"EA",replace:!0,scope:{content:"@",placement:"@",animation:"&",isOpen:"&"},templateUrl:"template/tooltip/tooltip-popup.html"}}).directive("tooltip",["$tooltip",function(a){return a("tooltip","tooltip","mouseenter")}]).directive("tooltipHtmlUnsafePopup",function(){return{restrict:"EA",replace:!0,scope:{content:"@",placement:"@",animation:"&",isOpen:"&"},templateUrl:"template/tooltip/tooltip-html-unsafe-popup.html"}}).directive("tooltipHtmlUnsafe",["$tooltip",function(a){return a("tooltipHtmlUnsafe","tooltip","mouseenter")}]),angular.module("ui.bootstrap.popover",["ui.bootstrap.tooltip"]).directive("popoverPopup",function(){return{restrict:"EA",replace:!0,scope:{title:"@",content:"@",placement:"@",animation:"&",isOpen:"&"},templateUrl:"template/popover/popover.html"}}).directive("popover",["$tooltip",function(a){return a("popover","popover","click")}]),angular.module("ui.bootstrap.progressbar",[]).constant("progressConfig",{animate:!0,max:100}).controller("ProgressController",["$scope","$attrs","progressConfig",function(a,b,c){var d=this,e=angular.isDefined(b.animate)?a.$parent.$eval(b.animate):c.animate;this.bars=[],a.max=angular.isDefined(b.max)?a.$parent.$eval(b.max):c.max,this.addBar=function(b,c){e||c.css({transition:"none"}),this.bars.push(b),b.$watch("value",function(c){b.percent=+(100*c/a.max).toFixed(2)}),b.$on("$destroy",function(){c=null,d.removeBar(b)})},this.removeBar=function(a){this.bars.splice(this.bars.indexOf(a),1)}}]).directive("progress",function(){return{restrict:"EA",replace:!0,transclude:!0,controller:"ProgressController",require:"progress",scope:{},templateUrl:"template/progressbar/progress.html"}}).directive("bar",function(){return{restrict:"EA",replace:!0,transclude:!0,require:"^progress",scope:{value:"=",type:"@"},templateUrl:"template/progressbar/bar.html",link:function(a,b,c,d){d.addBar(a,b)}}}).directive("progressbar",function(){return{restrict:"EA",replace:!0,transclude:!0,controller:"ProgressController",scope:{value:"=",type:"@"},templateUrl:"template/progressbar/progressbar.html",link:function(a,b,c,d){d.addBar(a,angular.element(b.children()[0]))}}}),angular.module("ui.bootstrap.rating",[]).constant("ratingConfig",{max:5,stateOn:null,stateOff:null}).controller("RatingController",["$scope","$attrs","ratingConfig",function(a,b,c){var d={$setViewValue:angular.noop};this.init=function(e){d=e,d.$render=this.render,this.stateOn=angular.isDefined(b.stateOn)?a.$parent.$eval(b.stateOn):c.stateOn,this.stateOff=angular.isDefined(b.stateOff)?a.$parent.$eval(b.stateOff):c.stateOff;var f=angular.isDefined(b.ratingStates)?a.$parent.$eval(b.ratingStates):new Array(angular.isDefined(b.max)?a.$parent.$eval(b.max):c.max);a.range=this.buildTemplateObjects(f)},this.buildTemplateObjects=function(a){for(var b=0,c=a.length;c>b;b++)a[b]=angular.extend({index:b},{stateOn:this.stateOn,stateOff:this.stateOff},a[b]);return a},a.rate=function(b){!a.readonly&&b>=0&&b<=a.range.length&&(d.$setViewValue(b),d.$render())},a.enter=function(b){a.readonly||(a.value=b),a.onHover({value:b})},a.reset=function(){a.value=d.$viewValue,a.onLeave()},a.onKeydown=function(b){/(37|38|39|40)/.test(b.which)&&(b.preventDefault(),b.stopPropagation(),a.rate(a.value+(38===b.which||39===b.which?1:-1)))},this.render=function(){a.value=d.$viewValue}}]).directive("rating",function(){return{restrict:"EA",require:["rating","ngModel"],scope:{readonly:"=?",onHover:"&",onLeave:"&"},controller:"RatingController",templateUrl:"template/rating/rating.html",replace:!0,link:function(a,b,c,d){var e=d[0],f=d[1];f&&e.init(f)}}}),angular.module("ui.bootstrap.tabs",[]).controller("TabsetController",["$scope",function(a){var b=this,c=b.tabs=a.tabs=[];b.select=function(a){angular.forEach(c,function(b){b.active&&b!==a&&(b.active=!1,b.onDeselect())}),a.active=!0,a.onSelect()},b.addTab=function(a){c.push(a),1===c.length?a.active=!0:a.active&&b.select(a)},b.removeTab=function(a){var d=c.indexOf(a);if(a.active&&c.length>1){var e=d==c.length-1?d-1:d+1;b.select(c[e])}c.splice(d,1)}}]).directive("tabset",function(){return{restrict:"EA",transclude:!0,replace:!0,scope:{type:"@"},controller:"TabsetController",templateUrl:"template/tabs/tabset.html",link:function(a,b,c){a.vertical=angular.isDefined(c.vertical)?a.$parent.$eval(c.vertical):!1,a.justified=angular.isDefined(c.justified)?a.$parent.$eval(c.justified):!1}}}).directive("tab",["$parse",function(a){return{require:"^tabset",restrict:"EA",replace:!0,templateUrl:"template/tabs/tab.html",transclude:!0,scope:{active:"=?",heading:"@",onSelect:"&select",onDeselect:"&deselect"},controller:function(){},compile:function(b,c,d){return function(b,c,e,f){b.$watch("active",function(a){a&&f.select(b)}),b.disabled=!1,e.disabled&&b.$parent.$watch(a(e.disabled),function(a){b.disabled=!!a}),b.select=function(){b.disabled||(b.active=!0)},f.addTab(b),b.$on("$destroy",function(){f.removeTab(b)}),b.$transcludeFn=d}}}}]).directive("tabHeadingTransclude",[function(){return{restrict:"A",require:"^tab",link:function(a,b){a.$watch("headingElement",function(a){a&&(b.html(""),b.append(a))})}}}]).directive("tabContentTransclude",function(){function a(a){return a.tagName&&(a.hasAttribute("tab-heading")||a.hasAttribute("data-tab-heading")||"tab-heading"===a.tagName.toLowerCase()||"data-tab-heading"===a.tagName.toLowerCase())}return{restrict:"A",require:"^tabset",link:function(b,c,d){var e=b.$eval(d.tabContentTransclude);e.$transcludeFn(e.$parent,function(b){angular.forEach(b,function(b){a(b)?e.headingElement=b:c.append(b)})})}}}),angular.module("ui.bootstrap.timepicker",[]).constant("timepickerConfig",{hourStep:1,minuteStep:1,showMeridian:!0,meridians:null,readonlyInput:!1,mousewheel:!0}).controller("TimepickerController",["$scope","$attrs","$parse","$log","$locale","timepickerConfig",function(a,b,c,d,e,f){function g(){var b=parseInt(a.hours,10),c=a.showMeridian?b>0&&13>b:b>=0&&24>b;return c?(a.showMeridian&&(12===b&&(b=0),a.meridian===p[1]&&(b+=12)),b):void 0}function h(){var b=parseInt(a.minutes,10);return b>=0&&60>b?b:void 0}function i(a){return angular.isDefined(a)&&a.toString().length<2?"0"+a:a}function j(a){k(),o.$setViewValue(new Date(n)),l(a)}function k(){o.$setValidity("time",!0),a.invalidHours=!1,a.invalidMinutes=!1}function l(b){var c=n.getHours(),d=n.getMinutes();a.showMeridian&&(c=0===c||12===c?12:c%12),a.hours="h"===b?c:i(c),a.minutes="m"===b?d:i(d),a.meridian=n.getHours()<12?p[0]:p[1]}function m(a){var b=new Date(n.getTime()+6e4*a);n.setHours(b.getHours(),b.getMinutes()),j()}var n=new Date,o={$setViewValue:angular.noop},p=angular.isDefined(b.meridians)?a.$parent.$eval(b.meridians):f.meridians||e.DATETIME_FORMATS.AMPMS;this.init=function(c,d){o=c,o.$render=this.render;var e=d.eq(0),g=d.eq(1),h=angular.isDefined(b.mousewheel)?a.$parent.$eval(b.mousewheel):f.mousewheel;h&&this.setupMousewheelEvents(e,g),a.readonlyInput=angular.isDefined(b.readonlyInput)?a.$parent.$eval(b.readonlyInput):f.readonlyInput,this.setupInputEvents(e,g)};var q=f.hourStep;b.hourStep&&a.$parent.$watch(c(b.hourStep),function(a){q=parseInt(a,10)});var r=f.minuteStep;b.minuteStep&&a.$parent.$watch(c(b.minuteStep),function(a){r=parseInt(a,10)}),a.showMeridian=f.showMeridian,b.showMeridian&&a.$parent.$watch(c(b.showMeridian),function(b){if(a.showMeridian=!!b,o.$error.time){var c=g(),d=h();angular.isDefined(c)&&angular.isDefined(d)&&(n.setHours(c),j())}else l()}),this.setupMousewheelEvents=function(b,c){var d=function(a){a.originalEvent&&(a=a.originalEvent);var b=a.wheelDelta?a.wheelDelta:-a.deltaY;return a.detail||b>0};b.bind("mousewheel wheel",function(b){a.$apply(d(b)?a.incrementHours():a.decrementHours()),b.preventDefault()}),c.bind("mousewheel wheel",function(b){a.$apply(d(b)?a.incrementMinutes():a.decrementMinutes()),b.preventDefault()})},this.setupInputEvents=function(b,c){if(a.readonlyInput)return a.updateHours=angular.noop,void(a.updateMinutes=angular.noop);var d=function(b,c){o.$setViewValue(null),o.$setValidity("time",!1),angular.isDefined(b)&&(a.invalidHours=b),angular.isDefined(c)&&(a.invalidMinutes=c)};a.updateHours=function(){var a=g();angular.isDefined(a)?(n.setHours(a),j("h")):d(!0)},b.bind("blur",function(){!a.invalidHours&&a.hours<10&&a.$apply(function(){a.hours=i(a.hours)})}),a.updateMinutes=function(){var a=h();angular.isDefined(a)?(n.setMinutes(a),j("m")):d(void 0,!0)},c.bind("blur",function(){!a.invalidMinutes&&a.minutes<10&&a.$apply(function(){a.minutes=i(a.minutes)})})},this.render=function(){var a=o.$modelValue?new Date(o.$modelValue):null;isNaN(a)?(o.$setValidity("time",!1),d.error('Timepicker directive: "ng-model" value must be a Date object, a number of milliseconds since 01.01.1970 or a string representing an RFC2822 or ISO 8601 date.')):(a&&(n=a),k(),l())},a.incrementHours=function(){m(60*q)},a.decrementHours=function(){m(60*-q)},a.incrementMinutes=function(){m(r)},a.decrementMinutes=function(){m(-r)},a.toggleMeridian=function(){m(720*(n.getHours()<12?1:-1))}}]).directive("timepicker",function(){return{restrict:"EA",require:["timepicker","?^ngModel"],controller:"TimepickerController",replace:!0,scope:{},templateUrl:"template/timepicker/timepicker.html",link:function(a,b,c,d){var e=d[0],f=d[1];f&&e.init(f,b.find("input"))}}}),angular.module("ui.bootstrap.typeahead",["ui.bootstrap.position","ui.bootstrap.bindHtml"]).factory("typeaheadParser",["$parse",function(a){var b=/^\s*(.*?)(?:\s+as\s+(.*?))?\s+for\s+(?:([\$\w][\$\w\d]*))\s+in\s+(.*)$/;return{parse:function(c){var d=c.match(b);if(!d)throw new Error('Expected typeahead specification in form of "_modelValue_ (as _label_)? for _item_ in _collection_" but got "'+c+'".');return{itemName:d[3],source:a(d[4]),viewMapper:a(d[2]||d[1]),modelMapper:a(d[1])}}}}]).directive("typeahead",["$compile","$parse","$q","$timeout","$document","$position","typeaheadParser",function(a,b,c,d,e,f,g){var h=[9,13,27,38,40];return{require:"ngModel",link:function(i,j,k,l){var m,n=i.$eval(k.typeaheadMinLength)||1,o=i.$eval(k.typeaheadWaitMs)||0,p=i.$eval(k.typeaheadEditable)!==!1,q=b(k.typeaheadLoading).assign||angular.noop,r=b(k.typeaheadOnSelect),s=k.typeaheadInputFormatter?b(k.typeaheadInputFormatter):void 0,t=k.typeaheadAppendToBody?i.$eval(k.typeaheadAppendToBody):!1,u=b(k.ngModel).assign,v=g.parse(k.typeahead),w=i.$new();i.$on("$destroy",function(){w.$destroy()});var x="typeahead-"+w.$id+"-"+Math.floor(1e4*Math.random());j.attr({"aria-autocomplete":"list","aria-expanded":!1,"aria-owns":x});var y=angular.element("
        ");y.attr({id:x,matches:"matches",active:"activeIdx",select:"select(activeIdx)",query:"query",position:"position"}),angular.isDefined(k.typeaheadTemplateUrl)&&y.attr("template-url",k.typeaheadTemplateUrl);var z=function(){w.matches=[],w.activeIdx=-1,j.attr("aria-expanded",!1)},A=function(a){return x+"-option-"+a};w.$watch("activeIdx",function(a){0>a?j.removeAttr("aria-activedescendant"):j.attr("aria-activedescendant",A(a))});var B=function(a){var b={$viewValue:a};q(i,!0),c.when(v.source(i,b)).then(function(c){var d=a===l.$viewValue;if(d&&m)if(c.length>0){w.activeIdx=0,w.matches.length=0;for(var e=0;e=n?o>0?(C&&d.cancel(C),C=d(function(){B(a)},o)):B(a):(q(i,!1),z()),p?a:a?void l.$setValidity("editable",!1):(l.$setValidity("editable",!0),a)}),l.$formatters.push(function(a){var b,c,d={};return s?(d.$model=a,s(i,d)):(d[v.itemName]=a,b=v.viewMapper(i,d),d[v.itemName]=void 0,c=v.viewMapper(i,d),b!==c?b:a)}),w.select=function(a){var b,c,e={};e[v.itemName]=c=w.matches[a].model,b=v.modelMapper(i,e),u(i,b),l.$setValidity("editable",!0),r(i,{$item:c,$model:b,$label:v.viewMapper(i,e)}),z(),d(function(){j[0].focus()},0,!1)},j.bind("keydown",function(a){0!==w.matches.length&&-1!==h.indexOf(a.which)&&(a.preventDefault(),40===a.which?(w.activeIdx=(w.activeIdx+1)%w.matches.length,w.$digest()):38===a.which?(w.activeIdx=(w.activeIdx?w.activeIdx:w.matches.length)-1,w.$digest()):13===a.which||9===a.which?w.$apply(function(){w.select(w.activeIdx)}):27===a.which&&(a.stopPropagation(),z(),w.$digest()))}),j.bind("blur",function(){m=!1});var D=function(a){j[0]!==a.target&&(z(),w.$digest())};e.bind("click",D),i.$on("$destroy",function(){e.unbind("click",D)});var E=a(y)(w);t?e.find("body").append(E):j.after(E)}}}]).directive("typeaheadPopup",function(){return{restrict:"EA",scope:{matches:"=",query:"=",active:"=",position:"=",select:"&"},replace:!0,templateUrl:"template/typeahead/typeahead-popup.html",link:function(a,b,c){a.templateUrl=c.templateUrl,a.isOpen=function(){return a.matches.length>0},a.isActive=function(b){return a.active==b},a.selectActive=function(b){a.active=b},a.selectMatch=function(b){a.select({activeIdx:b})}}}}).directive("typeaheadMatch",["$http","$templateCache","$compile","$parse",function(a,b,c,d){return{restrict:"EA",scope:{index:"=",match:"=",query:"="},link:function(e,f,g){var h=d(g.templateUrl)(e.$parent)||"template/typeahead/typeahead-match.html";a.get(h,{cache:b}).success(function(a){f.replaceWith(c(a.trim())(e))})}}}]).filter("typeaheadHighlight",function(){function a(a){return a.replace(/([.?*+^$[\]\\(){}|-])/g,"\\$1")}return function(b,c){return c?(""+b).replace(new RegExp(a(c),"gi"),"$&"):b}}),angular.module("template/accordion/accordion-group.html",[]).run(["$templateCache",function(a){a.put("template/accordion/accordion-group.html",'
        \n
        \n

        \n {{heading}}\n

        \n
        \n
        \n
        \n
        \n
        ')}]),angular.module("template/accordion/accordion.html",[]).run(["$templateCache",function(a){a.put("template/accordion/accordion.html",'
        ')}]),angular.module("template/alert/alert.html",[]).run(["$templateCache",function(a){a.put("template/alert/alert.html",'\n')}]),angular.module("template/carousel/carousel.html",[]).run(["$templateCache",function(a){a.put("template/carousel/carousel.html",'\n')}]),angular.module("template/carousel/slide.html",[]).run(["$templateCache",function(a){a.put("template/carousel/slide.html","
        \n")}]),angular.module("template/datepicker/datepicker.html",[]).run(["$templateCache",function(a){a.put("template/datepicker/datepicker.html",'
        \n \n \n \n
        ')}]),angular.module("template/datepicker/day.html",[]).run(["$templateCache",function(a){a.put("template/datepicker/day.html",'\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
        {{label.abbr}}
        {{ weekNumbers[$index] }}\n \n
        \n')}]),angular.module("template/datepicker/month.html",[]).run(["$templateCache",function(a){a.put("template/datepicker/month.html",'\n \n \n \n \n \n \n \n \n \n \n \n \n
        \n \n
        \n')}]),angular.module("template/datepicker/popup.html",[]).run(["$templateCache",function(a){a.put("template/datepicker/popup.html",'\n')}]),angular.module("template/datepicker/year.html",[]).run(["$templateCache",function(a){a.put("template/datepicker/year.html",'\n \n \n \n \n \n \n \n \n \n \n \n \n
        \n \n
        \n')}]),angular.module("template/modal/backdrop.html",[]).run(["$templateCache",function(a){a.put("template/modal/backdrop.html",'\n')}]),angular.module("template/modal/window.html",[]).run(["$templateCache",function(a){a.put("template/modal/window.html",'')}]),angular.module("template/pagination/pager.html",[]).run(["$templateCache",function(a){a.put("template/pagination/pager.html",'')}]),angular.module("template/pagination/pagination.html",[]).run(["$templateCache",function(a){a.put("template/pagination/pagination.html",'')}]),angular.module("template/tooltip/tooltip-html-unsafe-popup.html",[]).run(["$templateCache",function(a){a.put("template/tooltip/tooltip-html-unsafe-popup.html",'
        \n
        \n
        \n
        \n')}]),angular.module("template/tooltip/tooltip-popup.html",[]).run(["$templateCache",function(a){a.put("template/tooltip/tooltip-popup.html",'
        \n
        \n
        \n
        \n')}]),angular.module("template/popover/popover.html",[]).run(["$templateCache",function(a){a.put("template/popover/popover.html",'
        \n
        \n\n
        \n

        \n
        \n
        \n
        \n')}]),angular.module("template/progressbar/bar.html",[]).run(["$templateCache",function(a){a.put("template/progressbar/bar.html",'
        ')}]),angular.module("template/progressbar/progress.html",[]).run(["$templateCache",function(a){a.put("template/progressbar/progress.html",'
        ')}]),angular.module("template/progressbar/progressbar.html",[]).run(["$templateCache",function(a){a.put("template/progressbar/progressbar.html",'
        \n
        \n
        ')}]),angular.module("template/rating/rating.html",[]).run(["$templateCache",function(a){a.put("template/rating/rating.html",'\n \n ({{ $index < value ? \'*\' : \' \' }})\n \n')}]),angular.module("template/tabs/tab.html",[]).run(["$templateCache",function(a){a.put("template/tabs/tab.html",'
      • \n {{heading}}\n
      • \n')}]),angular.module("template/tabs/tabset-titles.html",[]).run(["$templateCache",function(a){a.put("template/tabs/tabset-titles.html","
          \n
        \n")}]),angular.module("template/tabs/tabset.html",[]).run(["$templateCache",function(a){a.put("template/tabs/tabset.html",'\n
        \n \n
        \n
        \n
        \n
        \n
        \n')}]),angular.module("template/timepicker/timepicker.html",[]).run(["$templateCache",function(a){a.put("template/timepicker/timepicker.html",'\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
         
        \n \n :\n \n
         
        \n')}]),angular.module("template/typeahead/typeahead-match.html",[]).run(["$templateCache",function(a){a.put("template/typeahead/typeahead-match.html",'')}]),angular.module("template/typeahead/typeahead-popup.html",[]).run(["$templateCache",function(a){a.put("template/typeahead/typeahead-popup.html",'') -}]); \ No newline at end of file + */angular.module("ui.bootstrap",["ui.bootstrap.tpls","ui.bootstrap.collapse","ui.bootstrap.accordion","ui.bootstrap.alert","ui.bootstrap.buttons","ui.bootstrap.carousel","ui.bootstrap.dateparser","ui.bootstrap.isClass","ui.bootstrap.datepicker","ui.bootstrap.position","ui.bootstrap.datepickerPopup","ui.bootstrap.debounce","ui.bootstrap.dropdown","ui.bootstrap.stackedMap","ui.bootstrap.modal","ui.bootstrap.paging","ui.bootstrap.pager","ui.bootstrap.pagination","ui.bootstrap.tooltip","ui.bootstrap.popover","ui.bootstrap.progressbar","ui.bootstrap.rating","ui.bootstrap.tabs","ui.bootstrap.timepicker","ui.bootstrap.typeahead"]),angular.module("ui.bootstrap.tpls",["uib/template/accordion/accordion-group.html","uib/template/accordion/accordion.html","uib/template/alert/alert.html","uib/template/carousel/carousel.html","uib/template/carousel/slide.html","uib/template/datepicker/datepicker.html","uib/template/datepicker/day.html","uib/template/datepicker/month.html","uib/template/datepicker/year.html","uib/template/datepickerPopup/popup.html","uib/template/modal/backdrop.html","uib/template/modal/window.html","uib/template/pager/pager.html","uib/template/pagination/pagination.html","uib/template/tooltip/tooltip-html-popup.html","uib/template/tooltip/tooltip-popup.html","uib/template/tooltip/tooltip-template-popup.html","uib/template/popover/popover-html.html","uib/template/popover/popover-template.html","uib/template/popover/popover.html","uib/template/progressbar/bar.html","uib/template/progressbar/progress.html","uib/template/progressbar/progressbar.html","uib/template/rating/rating.html","uib/template/tabs/tab.html","uib/template/tabs/tabset.html","uib/template/timepicker/timepicker.html","uib/template/typeahead/typeahead-match.html","uib/template/typeahead/typeahead-popup.html"]),angular.module("ui.bootstrap.collapse",[]).directive("uibCollapse",["$animate","$q","$parse","$injector",function(a,b,c,d){var e=d.has("$animateCss")?d.get("$animateCss"):null;return{link:function(d,f,g){function h(){f.hasClass("collapse")&&f.hasClass("in")||b.resolve(l(d)).then(function(){f.removeClass("collapse").addClass("collapsing").attr("aria-expanded",!0).attr("aria-hidden",!1),e?e(f,{addClass:"in",easing:"ease",to:{height:f[0].scrollHeight+"px"}}).start()["finally"](i):a.addClass(f,"in",{to:{height:f[0].scrollHeight+"px"}}).then(i)})}function i(){f.removeClass("collapsing").addClass("collapse").css({height:"auto"}),m(d)}function j(){return f.hasClass("collapse")||f.hasClass("in")?void b.resolve(n(d)).then(function(){f.css({height:f[0].scrollHeight+"px"}).removeClass("collapse").addClass("collapsing").attr("aria-expanded",!1).attr("aria-hidden",!0),e?e(f,{removeClass:"in",to:{height:"0"}}).start()["finally"](k):a.removeClass(f,"in",{to:{height:"0"}}).then(k)}):k()}function k(){f.css({height:"0"}),f.removeClass("collapsing").addClass("collapse"),o(d)}var l=c(g.expanding),m=c(g.expanded),n=c(g.collapsing),o=c(g.collapsed);d.$eval(g.uibCollapse)||f.addClass("in").addClass("collapse").attr("aria-expanded",!0).attr("aria-hidden",!1).css({height:"auto"}),d.$watch(g.uibCollapse,function(a){a?j():h()})}}}]),angular.module("ui.bootstrap.accordion",["ui.bootstrap.collapse"]).constant("uibAccordionConfig",{closeOthers:!0}).controller("UibAccordionController",["$scope","$attrs","uibAccordionConfig",function(a,b,c){this.groups=[],this.closeOthers=function(d){var e=angular.isDefined(b.closeOthers)?a.$eval(b.closeOthers):c.closeOthers;e&&angular.forEach(this.groups,function(a){a!==d&&(a.isOpen=!1)})},this.addGroup=function(a){var b=this;this.groups.push(a),a.$on("$destroy",function(c){b.removeGroup(a)})},this.removeGroup=function(a){var b=this.groups.indexOf(a);-1!==b&&this.groups.splice(b,1)}}]).directive("uibAccordion",function(){return{controller:"UibAccordionController",controllerAs:"accordion",transclude:!0,templateUrl:function(a,b){return b.templateUrl||"uib/template/accordion/accordion.html"}}}).directive("uibAccordionGroup",function(){return{require:"^uibAccordion",transclude:!0,replace:!0,templateUrl:function(a,b){return b.templateUrl||"uib/template/accordion/accordion-group.html"},scope:{heading:"@",panelClass:"@?",isOpen:"=?",isDisabled:"=?"},controller:function(){this.setHeading=function(a){this.heading=a}},link:function(a,b,c,d){d.addGroup(a),a.openClass=c.openClass||"panel-open",a.panelClass=c.panelClass||"panel-default",a.$watch("isOpen",function(c){b.toggleClass(a.openClass,!!c),c&&d.closeOthers(a)}),a.toggleOpen=function(b){a.isDisabled||b&&32!==b.which||(a.isOpen=!a.isOpen)};var e="accordiongroup-"+a.$id+"-"+Math.floor(1e4*Math.random());a.headingId=e+"-tab",a.panelId=e+"-panel"}}}).directive("uibAccordionHeading",function(){return{transclude:!0,template:"",replace:!0,require:"^uibAccordionGroup",link:function(a,b,c,d,e){d.setHeading(e(a,angular.noop))}}}).directive("uibAccordionTransclude",function(){return{require:"^uibAccordionGroup",link:function(a,b,c,d){a.$watch(function(){return d[c.uibAccordionTransclude]},function(a){if(a){var c=angular.element(b[0].querySelector("[uib-accordion-header]"));c.html(""),c.append(a)}})}}}),angular.module("ui.bootstrap.alert",[]).controller("UibAlertController",["$scope","$attrs","$interpolate","$timeout",function(a,b,c,d){a.closeable=!!b.close;var e=angular.isDefined(b.dismissOnTimeout)?c(b.dismissOnTimeout)(a.$parent):null;e&&d(function(){a.close()},parseInt(e,10))}]).directive("uibAlert",function(){return{controller:"UibAlertController",controllerAs:"alert",templateUrl:function(a,b){return b.templateUrl||"uib/template/alert/alert.html"},transclude:!0,replace:!0,scope:{type:"@",close:"&"}}}),angular.module("ui.bootstrap.buttons",[]).constant("uibButtonConfig",{activeClass:"active",toggleEvent:"click"}).controller("UibButtonsController",["uibButtonConfig",function(a){this.activeClass=a.activeClass||"active",this.toggleEvent=a.toggleEvent||"click"}]).directive("uibBtnRadio",["$parse",function(a){return{require:["uibBtnRadio","ngModel"],controller:"UibButtonsController",controllerAs:"buttons",link:function(b,c,d,e){var f=e[0],g=e[1],h=a(d.uibUncheckable);c.find("input").css({display:"none"}),g.$render=function(){c.toggleClass(f.activeClass,angular.equals(g.$modelValue,b.$eval(d.uibBtnRadio)))},c.on(f.toggleEvent,function(){if(!d.disabled){var a=c.hasClass(f.activeClass);a&&!angular.isDefined(d.uncheckable)||b.$apply(function(){g.$setViewValue(a?null:b.$eval(d.uibBtnRadio)),g.$render()})}}),d.uibUncheckable&&b.$watch(h,function(a){d.$set("uncheckable",a?"":void 0)})}}}]).directive("uibBtnCheckbox",function(){return{require:["uibBtnCheckbox","ngModel"],controller:"UibButtonsController",controllerAs:"button",link:function(a,b,c,d){function e(){return g(c.btnCheckboxTrue,!0)}function f(){return g(c.btnCheckboxFalse,!1)}function g(b,c){return angular.isDefined(b)?a.$eval(b):c}var h=d[0],i=d[1];b.find("input").css({display:"none"}),i.$render=function(){b.toggleClass(h.activeClass,angular.equals(i.$modelValue,e()))},b.on(h.toggleEvent,function(){c.disabled||a.$apply(function(){i.$setViewValue(b.hasClass(h.activeClass)?f():e()),i.$render()})})}}}),angular.module("ui.bootstrap.carousel",[]).controller("UibCarouselController",["$scope","$element","$interval","$timeout","$animate",function(a,b,c,d,e){function f(){for(;t.length;)t.shift()}function g(a){for(var b=0;b1){q[d].element.data(r,c.direction);var j=p.getCurrentIndex();angular.isNumber(j)&&q[j].element&&q[j].element.data(r,c.direction),a.$currentTransition=!0,e.on("addClass",q[d].element,function(b,c){if("close"===c&&(a.$currentTransition=null,e.off("addClass",b),t.length)){var d=t.pop().slide,g=d.index,i=g>p.getCurrentIndex()?"next":"prev";f(),h(d,g,i)}})}a.active=c.index,s=c.index,g(d),l()}}function i(a){for(var b=0;b0&&(n=c(m,b))}function m(){var b=+a.interval;o&&!isNaN(b)&&b>0&&q.length?a.next():a.pause()}var n,o,p=this,q=p.slides=a.slides=[],r="uib-slideDirection",s=a.active,t=[],u=!1;p.addSlide=function(b,c){q.push({slide:b,element:c}),q.sort(function(a,b){return+a.slide.index-+b.slide.index}),(b.index===a.active||1===q.length&&!angular.isNumber(a.active))&&(a.$currentTransition&&(a.$currentTransition=null),s=b.index,a.active=b.index,g(s),p.select(q[i(b)]),1===q.length&&a.play())},p.getCurrentIndex=function(){for(var a=0;a0&&s===c?c>=q.length?(s=q.length-1,a.active=s,g(s),p.select(q[q.length-1])):(s=c,a.active=s,g(s),p.select(q[c])):s>c&&(s--,a.active=s),0===q.length&&(s=null,a.active=null,f())},p.select=a.select=function(b,c){var d=i(b.slide);void 0===c&&(c=d>p.getCurrentIndex()?"next":"prev"),b.slide.index===s||a.$currentTransition?b&&b.slide.index!==s&&a.$currentTransition&&t.push(q[d]):h(b.slide,d,c)},a.indexOfSlide=function(a){return+a.slide.index},a.isActive=function(b){return a.active===b.slide.index},a.isPrevDisabled=function(){return 0===a.active&&a.noWrap()},a.isNextDisabled=function(){return a.active===q.length-1&&a.noWrap()},a.pause=function(){a.noPause||(o=!1,j())},a.play=function(){o||(o=!0,l())},a.$on("$destroy",function(){u=!0,j()}),a.$watch("noTransition",function(a){e.enabled(b,!a)}),a.$watch("interval",l),a.$watchCollection("slides",k),a.$watch("active",function(a){if(angular.isNumber(a)&&s!==a){for(var b=0;b-1){var g=!1;a=a.split("");for(var h=f;h-1){a=a.split(""),e[f]="("+d.regex+")",a[f]="$";for(var g=f+1,h=f+d.key.length;h>g;g++)e[g]="",a[g]="$";a=a.join(""),c.push({index:f,key:d.key,apply:d[b],matcher:d.regex})}}),{regex:new RegExp("^"+e.join("")+"$"),map:d(c,"index")}}function f(a,b,c){return 1>c?!1:1===b&&c>28?29===c&&(a%4===0&&a%100!==0||a%400===0):3===b||5===b||8===b||10===b?31>c:!0}function g(a){return parseInt(a,10)}function h(a,b){return a&&b?l(a,b):a}function i(a,b){return a&&b?l(a,b,!0):a}function j(a,b){var c=Date.parse("Jan 01, 1970 00:00:00 "+a)/6e4;return isNaN(c)?b:c}function k(a,b){return a=new Date(a.getTime()),a.setMinutes(a.getMinutes()+b),a}function l(a,b,c){c=c?-1:1;var d=j(b,a.getTimezoneOffset());return k(a,c*(d-a.getTimezoneOffset()))}var m,n,o=/[\\\^\$\*\+\?\|\[\]\(\)\.\{\}]/g;this.init=function(){m=b.id,this.parsers={},this.formatters={},n=[{key:"yyyy",regex:"\\d{4}",apply:function(a){this.year=+a},formatter:function(a){var b=new Date;return b.setFullYear(Math.abs(a.getFullYear())),c(b,"yyyy")}},{key:"yy",regex:"\\d{2}",apply:function(a){a=+a,this.year=69>a?a+2e3:a+1900},formatter:function(a){var b=new Date;return b.setFullYear(Math.abs(a.getFullYear())),c(b,"yy")}},{key:"y",regex:"\\d{1,4}",apply:function(a){this.year=+a},formatter:function(a){var b=new Date;return b.setFullYear(Math.abs(a.getFullYear())),c(b,"y")}},{key:"M!",regex:"0?[1-9]|1[0-2]",apply:function(a){this.month=a-1},formatter:function(a){var b=a.getMonth();return/^[0-9]$/.test(b)?c(a,"MM"):c(a,"M")}},{key:"MMMM",regex:b.DATETIME_FORMATS.MONTH.join("|"),apply:function(a){this.month=b.DATETIME_FORMATS.MONTH.indexOf(a)},formatter:function(a){return c(a,"MMMM")}},{key:"MMM",regex:b.DATETIME_FORMATS.SHORTMONTH.join("|"),apply:function(a){this.month=b.DATETIME_FORMATS.SHORTMONTH.indexOf(a)},formatter:function(a){return c(a,"MMM")}},{key:"MM",regex:"0[1-9]|1[0-2]",apply:function(a){this.month=a-1},formatter:function(a){return c(a,"MM")}},{key:"M",regex:"[1-9]|1[0-2]",apply:function(a){this.month=a-1},formatter:function(a){return c(a,"M")}},{key:"d!",regex:"[0-2]?[0-9]{1}|3[0-1]{1}",apply:function(a){this.date=+a},formatter:function(a){var b=a.getDate();return/^[1-9]$/.test(b)?c(a,"dd"):c(a,"d")}},{key:"dd",regex:"[0-2][0-9]{1}|3[0-1]{1}",apply:function(a){this.date=+a},formatter:function(a){return c(a,"dd")}},{key:"d",regex:"[1-2]?[0-9]{1}|3[0-1]{1}",apply:function(a){this.date=+a},formatter:function(a){return c(a,"d")}},{key:"EEEE",regex:b.DATETIME_FORMATS.DAY.join("|"),formatter:function(a){return c(a,"EEEE")}},{key:"EEE",regex:b.DATETIME_FORMATS.SHORTDAY.join("|"),formatter:function(a){return c(a,"EEE")}},{key:"HH",regex:"(?:0|1)[0-9]|2[0-3]",apply:function(a){this.hours=+a},formatter:function(a){return c(a,"HH")}},{key:"hh",regex:"0[0-9]|1[0-2]",apply:function(a){this.hours=+a},formatter:function(a){return c(a,"hh")}},{key:"H",regex:"1?[0-9]|2[0-3]",apply:function(a){this.hours=+a},formatter:function(a){return c(a,"H")}},{key:"h",regex:"[0-9]|1[0-2]",apply:function(a){this.hours=+a},formatter:function(a){return c(a,"h")}},{key:"mm",regex:"[0-5][0-9]",apply:function(a){this.minutes=+a},formatter:function(a){return c(a,"mm")}},{key:"m",regex:"[0-9]|[1-5][0-9]",apply:function(a){this.minutes=+a},formatter:function(a){return c(a,"m")}},{key:"sss",regex:"[0-9][0-9][0-9]",apply:function(a){this.milliseconds=+a},formatter:function(a){return c(a,"sss")}},{key:"ss",regex:"[0-5][0-9]",apply:function(a){this.seconds=+a},formatter:function(a){return c(a,"ss")}},{key:"s",regex:"[0-9]|[1-5][0-9]",apply:function(a){this.seconds=+a},formatter:function(a){return c(a,"s")}},{key:"a",regex:b.DATETIME_FORMATS.AMPMS.join("|"),apply:function(a){12===this.hours&&(this.hours=0),"PM"===a&&(this.hours+=12)},formatter:function(a){return c(a,"a")}},{key:"Z",regex:"[+-]\\d{4}",apply:function(a){var b=a.match(/([+-])(\d{2})(\d{2})/),c=b[1],d=b[2],e=b[3];this.hours+=g(c+d),this.minutes+=g(c+e)},formatter:function(a){return c(a,"Z")}},{key:"ww",regex:"[0-4][0-9]|5[0-3]",formatter:function(a){return c(a,"ww")}},{key:"w",regex:"[0-9]|[1-4][0-9]|5[0-3]",formatter:function(a){return c(a,"w")}},{key:"GGGG",regex:b.DATETIME_FORMATS.ERANAMES.join("|").replace(/\s/g,"\\s"),formatter:function(a){return c(a,"GGGG")}},{key:"GGG",regex:b.DATETIME_FORMATS.ERAS.join("|"),formatter:function(a){return c(a,"GGG")}},{key:"GG",regex:b.DATETIME_FORMATS.ERAS.join("|"),formatter:function(a){return c(a,"GG")}},{key:"G",regex:b.DATETIME_FORMATS.ERAS.join("|"),formatter:function(a){return c(a,"G")}}]},this.init(),this.filter=function(a,c){if(!angular.isDate(a)||isNaN(a)||!c)return"";c=b.DATETIME_FORMATS[c]||c,b.id!==m&&this.init(),this.formatters[c]||(this.formatters[c]=e(c,"formatter"));var d=this.formatters[c],f=d.map,g=c;return f.reduce(function(b,c,d){var e=g.match(new RegExp("(.*)"+c.key));e&&angular.isString(e[1])&&(b+=e[1],g=g.replace(e[1]+c.key,""));var h=d===f.length-1?g:"";return c.apply?b+c.apply.call(null,a)+h:b+h},"")},this.parse=function(c,d,g){if(!angular.isString(c)||!d)return c;d=b.DATETIME_FORMATS[d]||d,d=d.replace(o,"\\$&"),b.id!==m&&this.init(),this.parsers[d]||(this.parsers[d]=e(d,"apply"));var h=this.parsers[d],i=h.regex,j=h.map,k=c.match(i),l=!1;if(k&&k.length){var n,p;angular.isDate(g)&&!isNaN(g.getTime())?n={year:g.getFullYear(),month:g.getMonth(),date:g.getDate(),hours:g.getHours(),minutes:g.getMinutes(),seconds:g.getSeconds(),milliseconds:g.getMilliseconds()}:(g&&a.warn("dateparser:","baseDate is not a valid date"),n={year:1900,month:0,date:1,hours:0,minutes:0,seconds:0,milliseconds:0});for(var q=1,r=k.length;r>q;q++){var s=j[q-1];"Z"===s.matcher&&(l=!0),s.apply&&s.apply.call(n,k[q])}var t=l?Date.prototype.setUTCFullYear:Date.prototype.setFullYear,u=l?Date.prototype.setUTCHours:Date.prototype.setHours;return f(n.year,n.month,n.date)&&(!angular.isDate(g)||isNaN(g.getTime())||l?(p=new Date(0),t.call(p,n.year,n.month,n.date),u.call(p,n.hours||0,n.minutes||0,n.seconds||0,n.milliseconds||0)):(p=new Date(g),t.call(p,n.year,n.month,n.date),u.call(p,n.hours,n.minutes,n.seconds,n.milliseconds))),p}},this.toTimezone=h,this.fromTimezone=i,this.timezoneToOffset=j,this.addDateMinutes=k,this.convertTimezoneToLocal=l}]),angular.module("ui.bootstrap.isClass",[]).directive("uibIsClass",["$animate",function(a){var b=/^\s*([\s\S]+?)\s+on\s+([\s\S]+?)\s*$/,c=/^\s*([\s\S]+?)\s+for\s+([\s\S]+?)\s*$/;return{restrict:"A",compile:function(d,e){function f(a,b,c){i.push(a),j.push({scope:a,element:b}),o.forEach(function(b,c){g(b,a)}),a.$on("$destroy",h)}function g(b,d){var e=b.match(c),f=d.$eval(e[1]),g=e[2],h=k[b];if(!h){var i=function(b){var c=null;j.some(function(a){var d=a.scope.$eval(m);return d===b?(c=a,!0):void 0}),h.lastActivated!==c&&(h.lastActivated&&a.removeClass(h.lastActivated.element,f),c&&a.addClass(c.element,f),h.lastActivated=c)};k[b]=h={lastActivated:null,scope:d,watchFn:i,compareWithExp:g,watcher:d.$watch(g,i)}}h.watchFn(d.$eval(g))}function h(a){var b=a.targetScope,c=i.indexOf(b);if(i.splice(c,1),j.splice(c,1),i.length){var d=i[0];angular.forEach(k,function(a){a.scope===b&&(a.watcher=d.$watch(a.compareWithExp,a.watchFn),a.scope=d)})}else k={}}var i=[],j=[],k={},l=e.uibIsClass.match(b),m=l[2],n=l[1],o=n.split(",");return f}}}]),angular.module("ui.bootstrap.datepicker",["ui.bootstrap.dateparser","ui.bootstrap.isClass"]).value("$datepickerSuppressError",!1).value("$datepickerLiteralWarning",!0).constant("uibDatepickerConfig",{datepickerMode:"day",formatDay:"dd",formatMonth:"MMMM",formatYear:"yyyy",formatDayHeader:"EEE",formatDayTitle:"MMMM yyyy",formatMonthTitle:"yyyy",maxDate:null,maxMode:"year",minDate:null,minMode:"day",ngModelOptions:{},shortcutPropagation:!1,showWeeks:!0,yearColumns:5,yearRows:4}).controller("UibDatepickerController",["$scope","$attrs","$parse","$interpolate","$locale","$log","dateFilter","uibDatepickerConfig","$datepickerLiteralWarning","$datepickerSuppressError","uibDateParser",function(a,b,c,d,e,f,g,h,i,j,k){function l(b){a.datepickerMode=b,a.datepickerOptions.datepickerMode=b}var m=this,n={$setViewValue:angular.noop},o={},p=[];!!b.datepickerOptions;a.datepickerOptions||(a.datepickerOptions={}),this.modes=["day","month","year"],["customClass","dateDisabled","datepickerMode","formatDay","formatDayHeader","formatDayTitle","formatMonth","formatMonthTitle","formatYear","maxDate","maxMode","minDate","minMode","showWeeks","shortcutPropagation","startingDay","yearColumns","yearRows"].forEach(function(b){switch(b){case"customClass":case"dateDisabled":a[b]=a.datepickerOptions[b]||angular.noop;break;case"datepickerMode":a.datepickerMode=angular.isDefined(a.datepickerOptions.datepickerMode)?a.datepickerOptions.datepickerMode:h.datepickerMode;break;case"formatDay":case"formatDayHeader":case"formatDayTitle":case"formatMonth":case"formatMonthTitle":case"formatYear":m[b]=angular.isDefined(a.datepickerOptions[b])?d(a.datepickerOptions[b])(a.$parent):h[b];break;case"showWeeks":case"shortcutPropagation":case"yearColumns":case"yearRows":m[b]=angular.isDefined(a.datepickerOptions[b])?a.datepickerOptions[b]:h[b];break;case"startingDay":angular.isDefined(a.datepickerOptions.startingDay)?m.startingDay=a.datepickerOptions.startingDay:angular.isNumber(h.startingDay)?m.startingDay=h.startingDay:m.startingDay=(e.DATETIME_FORMATS.FIRSTDAYOFWEEK+8)%7;break;case"maxDate":case"minDate":a.$watch("datepickerOptions."+b,function(a){a?angular.isDate(a)?m[b]=k.fromTimezone(new Date(a),o.timezone):(i&&f.warn("Literal date support has been deprecated, please switch to date object usage"),m[b]=new Date(g(a,"medium"))):m[b]=h[b]?k.fromTimezone(new Date(h[b]),o.timezone):null,m.refreshView()});break;case"maxMode":case"minMode":a.datepickerOptions[b]?a.$watch(function(){return a.datepickerOptions[b]},function(c){m[b]=a[b]=angular.isDefined(c)?c:datepickerOptions[b],("minMode"===b&&m.modes.indexOf(a.datepickerOptions.datepickerMode)m.modes.indexOf(m[b]))&&(a.datepickerMode=m[b],a.datepickerOptions.datepickerMode=m[b])}):m[b]=a[b]=h[b]||null}}),a.uniqueId="datepicker-"+a.$id+"-"+Math.floor(1e4*Math.random()),a.disabled=angular.isDefined(b.disabled)||!1,angular.isDefined(b.ngDisabled)&&p.push(a.$parent.$watch(b.ngDisabled,function(b){a.disabled=b,m.refreshView()})),a.isActive=function(b){return 0===m.compare(b.date,m.activeDate)?(a.activeDateId=b.uid,!0):!1},this.init=function(b){n=b,o=b.$options||h.ngModelOptions,a.datepickerOptions.initDate?(m.activeDate=k.fromTimezone(a.datepickerOptions.initDate,o.timezone)||new Date,a.$watch("datepickerOptions.initDate",function(a){a&&(n.$isEmpty(n.$modelValue)||n.$invalid)&&(m.activeDate=k.fromTimezone(a,o.timezone),m.refreshView())})):m.activeDate=new Date,this.activeDate=n.$modelValue?k.fromTimezone(new Date(n.$modelValue),o.timezone):k.fromTimezone(new Date,o.timezone),n.$render=function(){m.render()}},this.render=function(){if(n.$viewValue){var a=new Date(n.$viewValue),b=!isNaN(a);b?this.activeDate=k.fromTimezone(a,o.timezone):j||f.error('Datepicker directive: "ng-model" value must be a Date object')}this.refreshView()},this.refreshView=function(){if(this.element){a.selectedDt=null,this._refreshView(),a.activeDt&&(a.activeDateId=a.activeDt.uid);var b=n.$viewValue?new Date(n.$viewValue):null;b=k.fromTimezone(b,o.timezone),n.$setValidity("dateDisabled",!b||this.element&&!this.isDisabled(b))}},this.createDateObject=function(b,c){var d=n.$viewValue?new Date(n.$viewValue):null;d=k.fromTimezone(d,o.timezone);var e=new Date;e=k.fromTimezone(e,o.timezone);var f=this.compare(b,e),g={date:b,label:k.filter(b,c),selected:d&&0===this.compare(b,d),disabled:this.isDisabled(b),past:0>f,current:0===f,future:f>0,customClass:this.customClass(b)||null};return d&&0===this.compare(b,d)&&(a.selectedDt=g),m.activeDate&&0===this.compare(g.date,m.activeDate)&&(a.activeDt=g),g},this.isDisabled=function(b){return a.disabled||this.minDate&&this.compare(b,this.minDate)<0||this.maxDate&&this.compare(b,this.maxDate)>0||a.dateDisabled&&a.dateDisabled({date:b,mode:a.datepickerMode})},this.customClass=function(b){return a.customClass({date:b,mode:a.datepickerMode})},this.split=function(a,b){for(var c=[];a.length>0;)c.push(a.splice(0,b));return c},a.select=function(b){if(a.datepickerMode===m.minMode){var c=n.$viewValue?k.fromTimezone(new Date(n.$viewValue),o.timezone):new Date(0,0,0,0,0,0,0);c.setFullYear(b.getFullYear(),b.getMonth(),b.getDate()),c=k.toTimezone(c,o.timezone),n.$setViewValue(c),n.$render()}else m.activeDate=b,l(m.modes[m.modes.indexOf(a.datepickerMode)-1]),a.$emit("uib:datepicker.mode");a.$broadcast("uib:datepicker.focus")},a.move=function(a){var b=m.activeDate.getFullYear()+a*(m.step.years||0),c=m.activeDate.getMonth()+a*(m.step.months||0);m.activeDate.setFullYear(b,c,1),m.refreshView()},a.toggleMode=function(b){b=b||1,a.datepickerMode===m.maxMode&&1===b||a.datepickerMode===m.minMode&&-1===b||(l(m.modes[m.modes.indexOf(a.datepickerMode)+b]),a.$emit("uib:datepicker.mode"))},a.keys={13:"enter",32:"space",33:"pageup",34:"pagedown",35:"end",36:"home",37:"left",38:"up",39:"right",40:"down"};var q=function(){m.element[0].focus()};a.$on("uib:datepicker.focus",q),a.keydown=function(b){var c=a.keys[b.which];if(c&&!b.shiftKey&&!b.altKey&&!a.disabled)if(b.preventDefault(),m.shortcutPropagation||b.stopPropagation(),"enter"===c||"space"===c){if(m.isDisabled(m.activeDate))return;a.select(m.activeDate)}else!b.ctrlKey||"up"!==c&&"down"!==c?(m.handleKeyDown(c,b),m.refreshView()):a.toggleMode("up"===c?1:-1)},a.$on("$destroy",function(){for(;p.length;)p.shift()()})}]).controller("UibDaypickerController",["$scope","$element","dateFilter",function(a,b,c){function d(a,b){return 1!==b||a%4!==0||a%100===0&&a%400!==0?f[b]:29}function e(a){var b=new Date(a);b.setDate(b.getDate()+4-(b.getDay()||7));var c=b.getTime();return b.setMonth(0),b.setDate(1),Math.floor(Math.round((c-b)/864e5)/7)+1}var f=[31,28,31,30,31,30,31,31,30,31,30,31];this.step={months:1},this.element=b,this.init=function(b){angular.extend(b,this),a.showWeeks=b.showWeeks,b.refreshView()},this.getDates=function(a,b){for(var c,d=new Array(b),e=new Date(a),f=0;b>f;)c=new Date(e),d[f++]=c,e.setDate(e.getDate()+1);return d},this._refreshView=function(){var b=this.activeDate.getFullYear(),d=this.activeDate.getMonth(),f=new Date(this.activeDate);f.setFullYear(b,d,1);var g=this.startingDay-f.getDay(),h=g>0?7-g:-g,i=new Date(f);h>0&&i.setDate(-h+1);for(var j=this.getDates(i,42),k=0;42>k;k++)j[k]=angular.extend(this.createDateObject(j[k],this.formatDay),{secondary:j[k].getMonth()!==d,uid:a.uniqueId+"-"+k});a.labels=new Array(7);for(var l=0;7>l;l++)a.labels[l]={abbr:c(j[l].date,this.formatDayHeader),full:c(j[l].date,"EEEE")};if(a.title=c(this.activeDate,this.formatDayTitle),a.rows=this.split(j,7),a.showWeeks){a.weekNumbers=[];for(var m=(11-this.startingDay)%7,n=a.rows.length,o=0;n>o;o++)a.weekNumbers.push(e(a.rows[o][m].date))}},this.compare=function(a,b){var c=new Date(a.getFullYear(),a.getMonth(),a.getDate()),d=new Date(b.getFullYear(),b.getMonth(),b.getDate());return c.setFullYear(a.getFullYear()),d.setFullYear(b.getFullYear()),c-d},this.handleKeyDown=function(a,b){var c=this.activeDate.getDate();if("left"===a)c-=1;else if("up"===a)c-=7;else if("right"===a)c+=1;else if("down"===a)c+=7;else if("pageup"===a||"pagedown"===a){var e=this.activeDate.getMonth()+("pageup"===a?-1:1);this.activeDate.setMonth(e,1),c=Math.min(d(this.activeDate.getFullYear(),this.activeDate.getMonth()),c)}else"home"===a?c=1:"end"===a&&(c=d(this.activeDate.getFullYear(),this.activeDate.getMonth()));this.activeDate.setDate(c)}}]).controller("UibMonthpickerController",["$scope","$element","dateFilter",function(a,b,c){this.step={years:1},this.element=b,this.init=function(a){angular.extend(a,this),a.refreshView()},this._refreshView=function(){for(var b,d=new Array(12),e=this.activeDate.getFullYear(),f=0;12>f;f++)b=new Date(this.activeDate),b.setFullYear(e,f,1),d[f]=angular.extend(this.createDateObject(b,this.formatMonth),{uid:a.uniqueId+"-"+f});a.title=c(this.activeDate,this.formatMonthTitle),a.rows=this.split(d,3)},this.compare=function(a,b){var c=new Date(a.getFullYear(),a.getMonth()),d=new Date(b.getFullYear(),b.getMonth());return c.setFullYear(a.getFullYear()),d.setFullYear(b.getFullYear()),c-d},this.handleKeyDown=function(a,b){var c=this.activeDate.getMonth();if("left"===a)c-=1;else if("up"===a)c-=3;else if("right"===a)c+=1;else if("down"===a)c+=3;else if("pageup"===a||"pagedown"===a){var d=this.activeDate.getFullYear()+("pageup"===a?-1:1);this.activeDate.setFullYear(d)}else"home"===a?c=0:"end"===a&&(c=11);this.activeDate.setMonth(c)}}]).controller("UibYearpickerController",["$scope","$element","dateFilter",function(a,b,c){function d(a){return parseInt((a-1)/f,10)*f+1}var e,f;this.element=b,this.yearpickerInit=function(){e=this.yearColumns,f=this.yearRows*e,this.step={years:f}},this._refreshView=function(){for(var b,c=new Array(f),g=0,h=d(this.activeDate.getFullYear());f>g;g++)b=new Date(this.activeDate),b.setFullYear(h+g,0,1),c[g]=angular.extend(this.createDateObject(b,this.formatYear),{uid:a.uniqueId+"-"+g});a.title=[c[0].label,c[f-1].label].join(" - "),a.rows=this.split(c,e),a.columns=e},this.compare=function(a,b){return a.getFullYear()-b.getFullYear()},this.handleKeyDown=function(a,b){var c=this.activeDate.getFullYear();"left"===a?c-=1:"up"===a?c-=e:"right"===a?c+=1:"down"===a?c+=e:"pageup"===a||"pagedown"===a?c+=("pageup"===a?-1:1)*f:"home"===a?c=d(this.activeDate.getFullYear()):"end"===a&&(c=d(this.activeDate.getFullYear())+f-1),this.activeDate.setFullYear(c)}}]).directive("uibDatepicker",function(){return{replace:!0,templateUrl:function(a,b){return b.templateUrl||"uib/template/datepicker/datepicker.html"},scope:{datepickerOptions:"=?"},require:["uibDatepicker","^ngModel"],controller:"UibDatepickerController",controllerAs:"datepicker",link:function(a,b,c,d){var e=d[0],f=d[1];e.init(f)}}}).directive("uibDaypicker",function(){return{replace:!0,templateUrl:function(a,b){return b.templateUrl||"uib/template/datepicker/day.html"},require:["^uibDatepicker","uibDaypicker"],controller:"UibDaypickerController",link:function(a,b,c,d){var e=d[0],f=d[1];f.init(e)}}}).directive("uibMonthpicker",function(){return{replace:!0,templateUrl:function(a,b){return b.templateUrl||"uib/template/datepicker/month.html"},require:["^uibDatepicker","uibMonthpicker"],controller:"UibMonthpickerController",link:function(a,b,c,d){var e=d[0],f=d[1];f.init(e)}}}).directive("uibYearpicker",function(){return{replace:!0,templateUrl:function(a,b){return b.templateUrl||"uib/template/datepicker/year.html"},require:["^uibDatepicker","uibYearpicker"],controller:"UibYearpickerController",link:function(a,b,c,d){var e=d[0];angular.extend(e,d[1]),e.yearpickerInit(),e.refreshView()}}}),angular.module("ui.bootstrap.position",[]).factory("$uibPosition",["$document","$window",function(a,b){var c,d,e={normal:/(auto|scroll)/,hidden:/(auto|scroll|hidden)/},f={auto:/\s?auto?\s?/i,primary:/^(top|bottom|left|right)$/,secondary:/^(top|bottom|left|right|center)$/,vertical:/^(top|bottom)$/},g=/(HTML|BODY)/;return{getRawNode:function(a){return a.nodeName?a:a[0]||a},parseStyle:function(a){return a=parseFloat(a),isFinite(a)?a:0},offsetParent:function(c){function d(a){return"static"===(b.getComputedStyle(a).position||"static")}c=this.getRawNode(c);for(var e=c.offsetParent||a[0].documentElement;e&&e!==a[0].documentElement&&d(e);)e=e.offsetParent;return e||a[0].documentElement},scrollbarWidth:function(e){if(e){if(angular.isUndefined(d)){var f=a.find("body");f.addClass("uib-position-body-scrollbar-measure"),d=b.innerWidth-f[0].clientWidth,d=isFinite(d)?d:0,f.removeClass("uib-position-body-scrollbar-measure")}return d}if(angular.isUndefined(c)){var g=angular.element('
        ');a.find("body").append(g),c=g[0].offsetWidth-g[0].clientWidth,c=isFinite(c)?c:0,g.remove()}return c},scrollbarPadding:function(a){a=this.getRawNode(a);var c=b.getComputedStyle(a),d=this.parseStyle(c.paddingRight),e=this.parseStyle(c.paddingBottom),f=this.scrollParent(a,!1,!0),h=this.scrollbarWidth(f,g.test(f.tagName));return{scrollbarWidth:h,widthOverflow:f.scrollWidth>f.clientWidth,right:d+h,originalRight:d,heightOverflow:f.scrollHeight>f.clientHeight,bottom:e+h,originalBottom:e}},isScrollable:function(a,c){a=this.getRawNode(a);var d=c?e.hidden:e.normal,f=b.getComputedStyle(a);return d.test(f.overflow+f.overflowY+f.overflowX); +},scrollParent:function(c,d,f){c=this.getRawNode(c);var g=d?e.hidden:e.normal,h=a[0].documentElement,i=b.getComputedStyle(c);if(f&&g.test(i.overflow+i.overflowY+i.overflowX))return c;var j="absolute"===i.position,k=c.parentElement||h;if(k===h||"fixed"===i.position)return h;for(;k.parentElement&&k!==h;){var l=b.getComputedStyle(k);if(j&&"static"!==l.position&&(j=!1),!j&&g.test(l.overflow+l.overflowY+l.overflowX))break;k=k.parentElement}return k},position:function(c,d){c=this.getRawNode(c);var e=this.offset(c);if(d){var f=b.getComputedStyle(c);e.top-=this.parseStyle(f.marginTop),e.left-=this.parseStyle(f.marginLeft)}var g=this.offsetParent(c),h={top:0,left:0};return g!==a[0].documentElement&&(h=this.offset(g),h.top+=g.clientTop-g.scrollTop,h.left+=g.clientLeft-g.scrollLeft),{width:Math.round(angular.isNumber(e.width)?e.width:c.offsetWidth),height:Math.round(angular.isNumber(e.height)?e.height:c.offsetHeight),top:Math.round(e.top-h.top),left:Math.round(e.left-h.left)}},offset:function(c){c=this.getRawNode(c);var d=c.getBoundingClientRect();return{width:Math.round(angular.isNumber(d.width)?d.width:c.offsetWidth),height:Math.round(angular.isNumber(d.height)?d.height:c.offsetHeight),top:Math.round(d.top+(b.pageYOffset||a[0].documentElement.scrollTop)),left:Math.round(d.left+(b.pageXOffset||a[0].documentElement.scrollLeft))}},viewportOffset:function(c,d,e){c=this.getRawNode(c),e=e!==!1;var f=c.getBoundingClientRect(),g={top:0,left:0,bottom:0,right:0},h=d?a[0].documentElement:this.scrollParent(c),i=h.getBoundingClientRect();if(g.top=i.top+h.clientTop,g.left=i.left+h.clientLeft,h===a[0].documentElement&&(g.top+=b.pageYOffset,g.left+=b.pageXOffset),g.bottom=g.top+h.clientHeight,g.right=g.left+h.clientWidth,e){var j=b.getComputedStyle(h);g.top+=this.parseStyle(j.paddingTop),g.bottom-=this.parseStyle(j.paddingBottom),g.left+=this.parseStyle(j.paddingLeft),g.right-=this.parseStyle(j.paddingRight)}return{top:Math.round(f.top-g.top),bottom:Math.round(g.bottom-f.bottom),left:Math.round(f.left-g.left),right:Math.round(g.right-f.right)}},parsePlacement:function(a){var b=f.auto.test(a);return b&&(a=a.replace(f.auto,"")),a=a.split("-"),a[0]=a[0]||"top",f.primary.test(a[0])||(a[0]="top"),a[1]=a[1]||"center",f.secondary.test(a[1])||(a[1]="center"),b?a[2]=!0:a[2]=!1,a},positionElements:function(a,c,d,e){a=this.getRawNode(a),c=this.getRawNode(c);var g=angular.isDefined(c.offsetWidth)?c.offsetWidth:c.prop("offsetWidth"),h=angular.isDefined(c.offsetHeight)?c.offsetHeight:c.prop("offsetHeight");d=this.parsePlacement(d);var i=e?this.offset(a):this.position(a),j={top:0,left:0,placement:""};if(d[2]){var k=this.viewportOffset(a,e),l=b.getComputedStyle(c),m={width:g+Math.round(Math.abs(this.parseStyle(l.marginLeft)+this.parseStyle(l.marginRight))),height:h+Math.round(Math.abs(this.parseStyle(l.marginTop)+this.parseStyle(l.marginBottom)))};if(d[0]="top"===d[0]&&m.height>k.top&&m.height<=k.bottom?"bottom":"bottom"===d[0]&&m.height>k.bottom&&m.height<=k.top?"top":"left"===d[0]&&m.width>k.left&&m.width<=k.right?"right":"right"===d[0]&&m.width>k.right&&m.width<=k.left?"left":d[0],d[1]="top"===d[1]&&m.height-i.height>k.bottom&&m.height-i.height<=k.top?"bottom":"bottom"===d[1]&&m.height-i.height>k.top&&m.height-i.height<=k.bottom?"top":"left"===d[1]&&m.width-i.width>k.right&&m.width-i.width<=k.left?"right":"right"===d[1]&&m.width-i.width>k.left&&m.width-i.width<=k.right?"left":d[1],"center"===d[1])if(f.vertical.test(d[0])){var n=i.width/2-g/2;k.left+n<0&&m.width-i.width<=k.right?d[1]="left":k.right+n<0&&m.width-i.width<=k.left&&(d[1]="right")}else{var o=i.height/2-m.height/2;k.top+o<0&&m.height-i.height<=k.bottom?d[1]="top":k.bottom+o<0&&m.height-i.height<=k.top&&(d[1]="bottom")}}switch(d[0]){case"top":j.top=i.top-h;break;case"bottom":j.top=i.top+i.height;break;case"left":j.left=i.left-g;break;case"right":j.left=i.left+i.width}switch(d[1]){case"top":j.top=i.top;break;case"bottom":j.top=i.top+i.height-h;break;case"left":j.left=i.left;break;case"right":j.left=i.left+i.width-g;break;case"center":f.vertical.test(d[0])?j.left=i.left+i.width/2-g/2:j.top=i.top+i.height/2-h/2}return j.top=Math.round(j.top),j.left=Math.round(j.left),j.placement="center"===d[1]?d[0]:d[0]+"-"+d[1],j},positionArrow:function(a,c){a=this.getRawNode(a);var d=a.querySelector(".tooltip-inner, .popover-inner");if(d){var e=angular.element(d).hasClass("tooltip-inner"),g=e?a.querySelector(".tooltip-arrow"):a.querySelector(".arrow");if(g){var h={top:"",bottom:"",left:"",right:""};if(c=this.parsePlacement(c),"center"===c[1])return void angular.element(g).css(h);var i="border-"+c[0]+"-width",j=b.getComputedStyle(g)[i],k="border-";k+=f.vertical.test(c[0])?c[0]+"-"+c[1]:c[1]+"-"+c[0],k+="-radius";var l=b.getComputedStyle(e?d:a)[k];switch(c[0]){case"top":h.bottom=e?"0":"-"+j;break;case"bottom":h.top=e?"0":"-"+j;break;case"left":h.right=e?"0":"-"+j;break;case"right":h.left=e?"0":"-"+j}h[c[1]]=l,angular.element(g).css(h)}}}}}]),angular.module("ui.bootstrap.datepickerPopup",["ui.bootstrap.datepicker","ui.bootstrap.position"]).value("$datepickerPopupLiteralWarning",!0).constant("uibDatepickerPopupConfig",{altInputFormats:[],appendToBody:!1,clearText:"Clear",closeOnDateSelection:!0,closeText:"Done",currentText:"Today",datepickerPopup:"yyyy-MM-dd",datepickerPopupTemplateUrl:"uib/template/datepickerPopup/popup.html",datepickerTemplateUrl:"uib/template/datepicker/datepicker.html",html5Types:{date:"yyyy-MM-dd","datetime-local":"yyyy-MM-ddTHH:mm:ss.sss",month:"yyyy-MM"},onOpenFocus:!0,showButtonBar:!0,placement:"auto bottom-left"}).controller("UibDatepickerPopupController",["$scope","$element","$attrs","$compile","$log","$parse","$window","$document","$rootScope","$uibPosition","dateFilter","uibDateParser","uibDatepickerPopupConfig","$timeout","uibDatepickerConfig","$datepickerPopupLiteralWarning",function(a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p){function q(b){var c=l.parse(b,w,a.date);if(isNaN(c))for(var d=0;d
        "),G?(J=G.timezone,a.ngModelOptions=angular.copy(G),a.ngModelOptions.timezone=null,a.ngModelOptions.updateOnDefault===!0&&(a.ngModelOptions.updateOn=a.ngModelOptions.updateOn?a.ngModelOptions.updateOn+" default":"default"),C.attr("ng-model-options","ngModelOptions")):J=null,C.attr({"ng-model":"date","ng-change":"dateSelection(date)","template-url":A}),D=angular.element(C.children()[0]),D.attr("template-url",B),a.datepickerOptions||(a.datepickerOptions={}),K&&"month"===c.type&&(a.datepickerOptions.datepickerMode="month",a.datepickerOptions.minMode="month"),D.attr("datepicker-options","datepickerOptions"),K?F.$formatters.push(function(b){return a.date=l.fromTimezone(b,J),b}):(F.$$parserName="date",F.$validators.date=s,F.$parsers.unshift(r),F.$formatters.push(function(b){return F.$isEmpty(b)?(a.date=b,b):(a.date=l.fromTimezone(b,J),angular.isNumber(a.date)&&(a.date=new Date(a.date)),l.filter(a.date,w))})),F.$viewChangeListeners.push(function(){a.date=q(F.$viewValue)}),b.on("keydown",u),H=d(C)(a),C.remove(),y?h.find("body").append(H):b.after(H),a.$on("$destroy",function(){for(a.isOpen===!0&&(i.$$phase||a.$apply(function(){a.isOpen=!1})),H.remove(),b.off("keydown",u),h.off("click",t),E&&E.off("scroll",v),angular.element(g).off("resize",v);L.length;)L.shift()()})},a.getText=function(b){return a[b+"Text"]||m[b+"Text"]},a.isDisabled=function(b){"today"===b&&(b=l.fromTimezone(new Date,J));var c={};return angular.forEach(["minDate","maxDate"],function(b){a.datepickerOptions[b]?angular.isDate(a.datepickerOptions[b])?c[b]=l.fromTimezone(new Date(a.datepickerOptions[b]),J):(p&&e.warn("Literal date support has been deprecated, please switch to date object usage"),c[b]=new Date(k(a.datepickerOptions[b],"medium"))):c[b]=null}),a.datepickerOptions&&c.minDate&&a.compare(b,c.minDate)<0||c.maxDate&&a.compare(b,c.maxDate)>0},a.compare=function(a,b){return new Date(a.getFullYear(),a.getMonth(),a.getDate())-new Date(b.getFullYear(),b.getMonth(),b.getDate())},a.dateSelection=function(c){angular.isDefined(c)&&(a.date=c);var d=a.date?l.filter(a.date,w):null;b.val(d),F.$setViewValue(d),x&&(a.isOpen=!1,b[0].focus())},a.keydown=function(c){27===c.which&&(c.stopPropagation(),a.isOpen=!1,b[0].focus())},a.select=function(b,c){if(c.stopPropagation(),"today"===b){var d=new Date;angular.isDate(a.date)?(b=new Date(a.date),b.setFullYear(d.getFullYear(),d.getMonth(),d.getDate())):b=new Date(d.setHours(0,0,0,0))}a.dateSelection(b)},a.close=function(c){c.stopPropagation(),a.isOpen=!1,b[0].focus()},a.disabled=angular.isDefined(c.disabled)||!1,c.ngDisabled&&L.push(a.$parent.$watch(f(c.ngDisabled),function(b){a.disabled=b})),a.$watch("isOpen",function(d){d?a.disabled?a.isOpen=!1:n(function(){v(),z&&a.$broadcast("uib:datepicker.focus"),h.on("click",t);var d=c.popupPlacement?c.popupPlacement:m.placement;y||j.parsePlacement(d)[2]?(E=E||angular.element(j.scrollParent(b)),E&&E.on("scroll",v)):E=null,angular.element(g).on("resize",v)},0,!1):(h.off("click",t),E&&E.off("scroll",v),angular.element(g).off("resize",v))}),a.$on("uib:datepicker.mode",function(){n(v,0,!1)})}]).directive("uibDatepickerPopup",function(){return{require:["ngModel","uibDatepickerPopup"],controller:"UibDatepickerPopupController",scope:{datepickerOptions:"=?",isOpen:"=?",currentText:"@",clearText:"@",closeText:"@"},link:function(a,b,c,d){var e=d[0],f=d[1];f.init(e)}}}).directive("uibDatepickerPopupWrap",function(){return{replace:!0,transclude:!0,templateUrl:function(a,b){return b.templateUrl||"uib/template/datepickerPopup/popup.html"}}}),angular.module("ui.bootstrap.debounce",[]).factory("$$debounce",["$timeout",function(a){return function(b,c){var d;return function(){var e=this,f=Array.prototype.slice.call(arguments);d&&a.cancel(d),d=a(function(){b.apply(e,f)},c)}}}]),angular.module("ui.bootstrap.dropdown",["ui.bootstrap.position"]).constant("uibDropdownConfig",{appendToOpenClass:"uib-dropdown-open",openClass:"open"}).service("uibDropdownService",["$document","$rootScope",function(a,b){var c=null;this.open=function(b,f){c||(a.on("click",d),f.on("keydown",e)),c&&c!==b&&(c.isOpen=!1),c=b},this.close=function(b,f){c===b&&(c=null,a.off("click",d),f.off("keydown",e))};var d=function(a){if(c&&!(a&&"disabled"===c.getAutoClose()||a&&3===a.which)){var d=c.getToggleElement();if(!(a&&d&&d[0].contains(a.target))){var e=c.getDropdownElement();a&&"outsideClick"===c.getAutoClose()&&e&&e[0].contains(a.target)||(c.isOpen=!1,b.$$phase||c.$apply())}}},e=function(a){27===a.which?(a.stopPropagation(),c.focusToggleElement(),d()):c.isKeynavEnabled()&&-1!==[38,40].indexOf(a.which)&&c.isOpen&&(a.preventDefault(),a.stopPropagation(),c.focusDropdownEntry(a.which))}}]).controller("UibDropdownController",["$scope","$element","$attrs","$parse","uibDropdownConfig","uibDropdownService","$animate","$uibPosition","$document","$compile","$templateRequest",function(a,b,c,d,e,f,g,h,i,j,k){var l,m,n=this,o=a.$new(),p=e.appendToOpenClass,q=e.openClass,r=angular.noop,s=c.onToggle?d(c.onToggle):angular.noop,t=!1,u=null,v=!1,w=i.find("body");b.addClass("dropdown"),this.init=function(){if(c.isOpen&&(m=d(c.isOpen),r=m.assign,a.$watch(m,function(a){o.isOpen=!!a})),angular.isDefined(c.dropdownAppendTo)){var e=d(c.dropdownAppendTo)(o);e&&(u=angular.element(e))}t=angular.isDefined(c.dropdownAppendToBody),v=angular.isDefined(c.keyboardNav),t&&!u&&(u=w),u&&n.dropdownMenu&&(u.append(n.dropdownMenu),b.on("$destroy",function(){n.dropdownMenu.remove()}))},this.toggle=function(a){return o.isOpen=arguments.length?!!a:!o.isOpen,angular.isFunction(r)&&r(o,o.isOpen),o.isOpen},this.isOpen=function(){return o.isOpen},o.getToggleElement=function(){return n.toggleElement},o.getAutoClose=function(){return c.autoClose||"always"},o.getElement=function(){return b},o.isKeynavEnabled=function(){return v},o.focusDropdownEntry=function(a){var c=n.dropdownMenu?angular.element(n.dropdownMenu).find("a"):b.find("ul").eq(0).find("a");switch(a){case 40:angular.isNumber(n.selectedOption)?n.selectedOption=n.selectedOption===c.length-1?n.selectedOption:n.selectedOption+1:n.selectedOption=0;break;case 38:angular.isNumber(n.selectedOption)?n.selectedOption=0===n.selectedOption?0:n.selectedOption-1:n.selectedOption=c.length-1}c[n.selectedOption].focus()},o.getDropdownElement=function(){return n.dropdownMenu},o.focusToggleElement=function(){n.toggleElement&&n.toggleElement[0].focus()},o.$watch("isOpen",function(c,d){if(u&&n.dropdownMenu){var e,i,m=h.positionElements(b,n.dropdownMenu,"bottom-left",!0);if(e={top:m.top+"px",display:c?"block":"none"},i=n.dropdownMenu.hasClass("dropdown-menu-right"),i?(e.left="auto",e.right=window.innerWidth-(m.left+b.prop("offsetWidth"))+"px"):(e.left=m.left+"px",e.right="auto"),!t){var v=h.offset(u);e.top=m.top-v.top+"px",i?e.right=window.innerWidth-(m.left-v.left+b.prop("offsetWidth"))+"px":e.left=m.left-v.left+"px"}n.dropdownMenu.css(e)}var w=u?u:b,x=w.hasClass(u?p:q);if(x===!c&&g[c?"addClass":"removeClass"](w,u?p:q).then(function(){angular.isDefined(c)&&c!==d&&s(a,{open:!!c})}),c)n.dropdownMenuTemplateUrl&&k(n.dropdownMenuTemplateUrl).then(function(a){l=o.$new(),j(a.trim())(l,function(a){var b=a;n.dropdownMenu.replaceWith(b),n.dropdownMenu=b})}),o.focusToggleElement(),f.open(o,b);else{if(n.dropdownMenuTemplateUrl){l&&l.$destroy();var y=angular.element('');n.dropdownMenu.replaceWith(y),n.dropdownMenu=y}f.close(o,b),n.selectedOption=null}angular.isFunction(r)&&r(a,c)})}]).directive("uibDropdown",function(){return{controller:"UibDropdownController",link:function(a,b,c,d){d.init()}}}).directive("uibDropdownMenu",function(){return{restrict:"A",require:"?^uibDropdown",link:function(a,b,c,d){if(d&&!angular.isDefined(c.dropdownNested)){b.addClass("dropdown-menu");var e=c.templateUrl;e&&(d.dropdownMenuTemplateUrl=e),d.dropdownMenu||(d.dropdownMenu=b)}}}}).directive("uibDropdownToggle",function(){return{require:"?^uibDropdown",link:function(a,b,c,d){if(d){b.addClass("dropdown-toggle"),d.toggleElement=b;var e=function(e){e.preventDefault(),b.hasClass("disabled")||c.disabled||a.$apply(function(){d.toggle()})};b.bind("click",e),b.attr({"aria-haspopup":!0,"aria-expanded":!1}),a.$watch(d.isOpen,function(a){b.attr("aria-expanded",!!a)}),a.$on("$destroy",function(){b.unbind("click",e)})}}}}),angular.module("ui.bootstrap.stackedMap",[]).factory("$$stackedMap",function(){return{createNew:function(){var a=[];return{add:function(b,c){a.push({key:b,value:c})},get:function(b){for(var c=0;c-1&&y>a&&(a=y),a}function l(a,b){var c=v.get(a).value,d=c.appendTo;v.remove(a),z=v.top(),z&&(y=parseInt(z.value.modalDomEl.attr("index"),10)),o(c.modalDomEl,c.modalScope,function(){var b=c.openedClass||u;w.remove(b,a);var e=w.hasKey(b);d.toggleClass(b,e),!e&&t&&t.heightOverflow&&t.scrollbarWidth&&(t.originalRight?d.css({paddingRight:t.originalRight+"px"}):d.css({paddingRight:""}),t=null),m(!0)},c.closedDeferred),n(),b&&b.focus?b.focus():d.focus&&d.focus()}function m(a){var b;v.length()>0&&(b=v.top().value,b.modalDomEl.toggleClass(b.windowTopClass||"",a))}function n(){if(r&&-1===k()){var a=s;o(r,s,function(){a=null}),r=void 0,s=void 0}}function o(b,c,d,e){function g(){g.done||(g.done=!0,a.leave(b).then(function(){b.remove(),e&&e.resolve()}),c.$destroy(),d&&d())}var h,i=null,j=function(){return h||(h=f.defer(),i=h.promise),function(){h.resolve()}};return c.$broadcast(x.NOW_CLOSING_EVENT,j),f.when(i).then(g)}function p(a){if(a.isDefaultPrevented())return a;var b=v.top();if(b)switch(a.which){case 27:b.value.keyboard&&(a.preventDefault(),e.$apply(function(){x.dismiss(b.key,"escape key press")}));break;case 9:var c=x.loadFocusElementList(b),d=!1;a.shiftKey?(x.isFocusInFirstItem(a,c)||x.isModalFocused(a,b))&&(d=x.focusLastFocusableElement(c)):x.isFocusInLastItem(a,c)&&(d=x.focusFirstFocusableElement(c)),d&&(a.preventDefault(),a.stopPropagation())}}function q(a,b,c){return!a.value.modalScope.$broadcast("modal.closing",b,c).defaultPrevented}var r,s,t,u="modal-open",v=h.createNew(),w=g.createNew(),x={NOW_CLOSING_EVENT:"modal.stack.now-closing"},y=0,z=null,A="a[href], area[href], input:not([disabled]), button:not([disabled]),select:not([disabled]), textarea:not([disabled]), iframe, object, embed, *[tabindex], *[contenteditable=true]";return e.$watch(k,function(a){s&&(s.index=a)}),c.on("keydown",p),e.$on("$destroy",function(){c.off("keydown",p)}),x.open=function(b,f){var g=c[0].activeElement,h=f.openedClass||u;m(!1),z=v.top(),v.add(b,{deferred:f.deferred,renderDeferred:f.renderDeferred,closedDeferred:f.closedDeferred,modalScope:f.scope,backdrop:f.backdrop,keyboard:f.keyboard,openedClass:f.openedClass,windowTopClass:f.windowTopClass,animation:f.animation,appendTo:f.appendTo}),w.put(h,b);var j=f.appendTo,l=k();if(!j.length)throw new Error("appendTo element not found. Make sure that the element passed is in DOM.");l>=0&&!r&&(s=e.$new(!0),s.modalOptions=f,s.index=l,r=angular.element('
        '),r.attr("backdrop-class",f.backdropClass),f.animation&&r.attr("modal-animation","true"),d(r)(s),a.enter(r,j),t=i.scrollbarPadding(j),t.heightOverflow&&t.scrollbarWidth&&j.css({paddingRight:t.right+"px"})),y=z?parseInt(z.value.modalDomEl.attr("index"),10)+1:0;var n=angular.element('
        ');n.attr({"template-url":f.windowTemplateUrl,"window-class":f.windowClass,"window-top-class":f.windowTopClass,size:f.size,index:y,animate:"animate"}).html(f.content),f.animation&&n.attr("modal-animation","true"),j.addClass(h),a.enter(d(n)(f.scope),j),v.top().value.modalDomEl=n,v.top().value.modalOpener=g},x.close=function(a,b){var c=v.get(a);return c&&q(c,b,!0)?(c.value.modalScope.$$uibDestructionScheduled=!0,c.value.deferred.resolve(b),l(a,c.value.modalOpener),!0):!c},x.dismiss=function(a,b){var c=v.get(a);return c&&q(c,b,!1)?(c.value.modalScope.$$uibDestructionScheduled=!0,c.value.deferred.reject(b),l(a,c.value.modalOpener),!0):!c},x.dismissAll=function(a){for(var b=this.getTop();b&&this.dismiss(b.key,a);)b=this.getTop()},x.getTop=function(){return v.top()},x.modalRendered=function(a){var b=v.get(a);b&&b.value.renderDeferred.resolve()},x.focusFirstFocusableElement=function(a){return a.length>0?(a[0].focus(),!0):!1},x.focusLastFocusableElement=function(a){return a.length>0?(a[a.length-1].focus(),!0):!1},x.isModalFocused=function(a,b){if(a&&b){var c=b.value.modalDomEl;if(c&&c.length)return(a.target||a.srcElement)===c[0]}return!1},x.isFocusInFirstItem=function(a,b){return b.length>0?(a.target||a.srcElement)===b[0]:!1},x.isFocusInLastItem=function(a,b){return b.length>0?(a.target||a.srcElement)===b[b.length-1]:!1},x.loadFocusElementList=function(a){if(a){var b=a.value.modalDomEl;if(b&&b.length){var c=b[0].querySelectorAll(A);return c?Array.prototype.filter.call(c,function(a){return j(a)}):c}}},x}]).provider("$uibModal",function(){var a={options:{animation:!0,backdrop:!0,keyboard:!0},$get:["$rootScope","$q","$document","$templateRequest","$controller","$uibResolve","$uibModalStack",function(b,c,d,e,f,g,h){function i(a){return a.template?c.when(a.template):e(angular.isFunction(a.templateUrl)?a.templateUrl():a.templateUrl)}var j={},k=null;return j.getPromiseChain=function(){return k},j.open=function(e){function j(){return r}var l=c.defer(),m=c.defer(),n=c.defer(),o=c.defer(),p={result:l.promise,opened:m.promise,closed:n.promise,rendered:o.promise,close:function(a){return h.close(p,a)},dismiss:function(a){return h.dismiss(p,a)}};if(e=angular.extend({},a.options,e),e.resolve=e.resolve||{},e.appendTo=e.appendTo||d.find("body").eq(0),!e.template&&!e.templateUrl)throw new Error("One of template or templateUrl options is required.");var q,r=c.all([i(e),g.resolve(e.resolve,{},null,null)]);return q=k=c.all([k]).then(j,j).then(function(a){var c=e.scope||b,d=c.$new();d.$close=p.close,d.$dismiss=p.dismiss,d.$on("$destroy",function(){d.$$uibDestructionScheduled||d.$dismiss("$uibUnscheduledDestruction")});var g,i,j={};e.controller&&(j.$scope=d,j.$uibModalInstance=p,angular.forEach(a[1],function(a,b){j[b]=a}),i=f(e.controller,j,!0),e.controllerAs?(g=i.instance,e.bindToController&&(g.$close=d.$close,g.$dismiss=d.$dismiss,angular.extend(g,c)),g=i(),d[e.controllerAs]=g):g=i(),angular.isFunction(g.$onInit)&&g.$onInit()),h.open(p,{scope:d,deferred:l,renderDeferred:o,closedDeferred:n,content:a[0],animation:e.animation,backdrop:e.backdrop,keyboard:e.keyboard,backdropClass:e.backdropClass,windowTopClass:e.windowTopClass,windowClass:e.windowClass,windowTemplateUrl:e.windowTemplateUrl,size:e.size,openedClass:e.openedClass,appendTo:e.appendTo}),m.resolve(!0)},function(a){m.reject(a),l.reject(a)})["finally"](function(){k===q&&(k=null)}),p},j}]};return a}),angular.module("ui.bootstrap.paging",[]).factory("uibPaging",["$parse",function(a){return{create:function(b,c,d){b.setNumPages=d.numPages?a(d.numPages).assign:angular.noop,b.ngModelCtrl={$setViewValue:angular.noop},b._watchers=[],b.init=function(a,e){b.ngModelCtrl=a,b.config=e,a.$render=function(){b.render()},d.itemsPerPage?b._watchers.push(c.$parent.$watch(d.itemsPerPage,function(a){b.itemsPerPage=parseInt(a,10),c.totalPages=b.calculateTotalPages(),b.updatePage()})):b.itemsPerPage=e.itemsPerPage,c.$watch("totalItems",function(a,d){(angular.isDefined(a)||a!==d)&&(c.totalPages=b.calculateTotalPages(),b.updatePage())})},b.calculateTotalPages=function(){var a=b.itemsPerPage<1?1:Math.ceil(c.totalItems/b.itemsPerPage);return Math.max(a||0,1)},b.render=function(){c.page=parseInt(b.ngModelCtrl.$viewValue,10)||1},c.selectPage=function(a,d){d&&d.preventDefault();var e=!c.ngDisabled||!d;e&&c.page!==a&&a>0&&a<=c.totalPages&&(d&&d.target&&d.target.blur(),b.ngModelCtrl.$setViewValue(a),b.ngModelCtrl.$render())},c.getText=function(a){return c[a+"Text"]||b.config[a+"Text"]},c.noPrevious=function(){return 1===c.page},c.noNext=function(){return c.page===c.totalPages},b.updatePage=function(){b.setNumPages(c.$parent,c.totalPages),c.page>c.totalPages?c.selectPage(c.totalPages):b.ngModelCtrl.$render()},c.$on("$destroy",function(){for(;b._watchers.length;)b._watchers.shift()()})}}}]),angular.module("ui.bootstrap.pager",["ui.bootstrap.paging"]).controller("UibPagerController",["$scope","$attrs","uibPaging","uibPagerConfig",function(a,b,c,d){a.align=angular.isDefined(b.align)?a.$parent.$eval(b.align):d.align,c.create(this,a,b)}]).constant("uibPagerConfig",{itemsPerPage:10,previousText:"« Previous",nextText:"Next »",align:!0}).directive("uibPager",["uibPagerConfig",function(a){return{scope:{totalItems:"=",previousText:"@",nextText:"@",ngDisabled:"="},require:["uibPager","?ngModel"],controller:"UibPagerController",controllerAs:"pager",templateUrl:function(a,b){return b.templateUrl||"uib/template/pager/pager.html"},replace:!0,link:function(b,c,d,e){var f=e[0],g=e[1];g&&f.init(g,a)}}}]),angular.module("ui.bootstrap.pagination",["ui.bootstrap.paging"]).controller("UibPaginationController",["$scope","$attrs","$parse","uibPaging","uibPaginationConfig",function(a,b,c,d,e){function f(a,b,c){return{number:a,text:b,active:c}}function g(a,b){var c=[],d=1,e=b,g=angular.isDefined(i)&&b>i;g&&(j?(d=Math.max(a-Math.floor(i/2),1),e=d+i-1,e>b&&(e=b,d=e-i+1)):(d=(Math.ceil(a/i)-1)*i+1,e=Math.min(d+i-1,b)));for(var h=d;e>=h;h++){var n=f(h,m(h),h===a);c.push(n)}if(g&&i>0&&(!j||k||l)){if(d>1){if(!l||d>3){var o=f(d-1,"...",!1);c.unshift(o)}if(l){if(3===d){var p=f(2,"2",!1);c.unshift(p)}var q=f(1,"1",!1);c.unshift(q)}}if(b>e){if(!l||b-2>e){var r=f(e+1,"...",!1);c.push(r)}if(l){if(e===b-2){var s=f(b-1,b-1,!1);c.push(s)}var t=f(b,b,!1);c.push(t)}}}return c}var h=this,i=angular.isDefined(b.maxSize)?a.$parent.$eval(b.maxSize):e.maxSize,j=angular.isDefined(b.rotate)?a.$parent.$eval(b.rotate):e.rotate,k=angular.isDefined(b.forceEllipses)?a.$parent.$eval(b.forceEllipses):e.forceEllipses,l=angular.isDefined(b.boundaryLinkNumbers)?a.$parent.$eval(b.boundaryLinkNumbers):e.boundaryLinkNumbers,m=angular.isDefined(b.pageLabel)?function(c){return a.$parent.$eval(b.pageLabel,{$page:c})}:angular.identity;a.boundaryLinks=angular.isDefined(b.boundaryLinks)?a.$parent.$eval(b.boundaryLinks):e.boundaryLinks,a.directionLinks=angular.isDefined(b.directionLinks)?a.$parent.$eval(b.directionLinks):e.directionLinks,d.create(this,a,b),b.maxSize&&h._watchers.push(a.$parent.$watch(c(b.maxSize),function(a){i=parseInt(a,10),h.render()}));var n=this.render;this.render=function(){n(),a.page>0&&a.page<=a.totalPages&&(a.pages=g(a.page,a.totalPages))}}]).constant("uibPaginationConfig",{itemsPerPage:10,boundaryLinks:!1,boundaryLinkNumbers:!1,directionLinks:!0,firstText:"First",previousText:"Previous",nextText:"Next",lastText:"Last",rotate:!0,forceEllipses:!1}).directive("uibPagination",["$parse","uibPaginationConfig",function(a,b){return{scope:{totalItems:"=",firstText:"@",previousText:"@",nextText:"@",lastText:"@",ngDisabled:"="},require:["uibPagination","?ngModel"],controller:"UibPaginationController",controllerAs:"pagination",templateUrl:function(a,b){return b.templateUrl||"uib/template/pagination/pagination.html"},replace:!0,link:function(a,c,d,e){var f=e[0],g=e[1];g&&f.init(g,b)}}}]),angular.module("ui.bootstrap.tooltip",["ui.bootstrap.position","ui.bootstrap.stackedMap"]).provider("$uibTooltip",function(){function a(a){var b=/[A-Z]/g,c="-";return a.replace(b,function(a,b){return(b?c:"")+a.toLowerCase()})}var b={placement:"top",placementClassPrefix:"",animation:!0,popupDelay:0,popupCloseDelay:0,useContentExp:!1},c={mouseenter:"mouseleave",click:"click",outsideClick:"outsideClick",focus:"blur",none:""},d={};this.options=function(a){angular.extend(d,a)},this.setTriggers=function(a){angular.extend(c,a)},this.$get=["$window","$compile","$timeout","$document","$uibPosition","$interpolate","$rootScope","$parse","$$stackedMap",function(e,f,g,h,i,j,k,l,m){function n(a){if(27===a.which){var b=o.top();b&&(b.value.close(),o.removeTop(),b=null)}}var o=m.createNew();return h.on("keypress",n),k.$on("$destroy",function(){h.off("keypress",n)}),function(e,k,m,n){function p(a){var b=(a||n.trigger||m).split(" "),d=b.map(function(a){return c[a]||a});return{show:b,hide:d}}n=angular.extend({},b,d,n);var q=a(e),r=j.startSymbol(),s=j.endSymbol(),t="
        '; +return{compile:function(a,b){var c=f(t);return function(a,b,d,f){function j(){N.isOpen?q():m()}function m(){M&&!a.$eval(d[k+"Enable"])||(u(),x(),N.popupDelay?G||(G=g(r,N.popupDelay,!1)):r())}function q(){s(),N.popupCloseDelay?H||(H=g(t,N.popupCloseDelay,!1)):t()}function r(){return s(),u(),N.content?(v(),void N.$evalAsync(function(){N.isOpen=!0,y(!0),S()})):angular.noop}function s(){G&&(g.cancel(G),G=null),I&&(g.cancel(I),I=null)}function t(){N&&N.$evalAsync(function(){N&&(N.isOpen=!1,y(!1),N.animation?F||(F=g(w,150,!1)):w())})}function u(){H&&(g.cancel(H),H=null),F&&(g.cancel(F),F=null)}function v(){D||(E=N.$new(),D=c(E,function(a){K?h.find("body").append(a):b.after(a)}),z())}function w(){s(),u(),A(),D&&(D.remove(),D=null),E&&(E.$destroy(),E=null)}function x(){N.title=d[k+"Title"],Q?N.content=Q(a):N.content=d[e],N.popupClass=d[k+"Class"],N.placement=angular.isDefined(d[k+"Placement"])?d[k+"Placement"]:n.placement;var b=i.parsePlacement(N.placement);J=b[1]?b[0]+"-"+b[1]:b[0];var c=parseInt(d[k+"PopupDelay"],10),f=parseInt(d[k+"PopupCloseDelay"],10);N.popupDelay=isNaN(c)?n.popupDelay:c,N.popupCloseDelay=isNaN(f)?n.popupCloseDelay:f}function y(b){P&&angular.isFunction(P.assign)&&P.assign(a,b)}function z(){R.length=0,Q?(R.push(a.$watch(Q,function(a){N.content=a,!a&&N.isOpen&&t()})),R.push(E.$watch(function(){O||(O=!0,E.$$postDigest(function(){O=!1,N&&N.isOpen&&S()}))}))):R.push(d.$observe(e,function(a){N.content=a,!a&&N.isOpen?t():S()})),R.push(d.$observe(k+"Title",function(a){N.title=a,N.isOpen&&S()})),R.push(d.$observe(k+"Placement",function(a){N.placement=a?a:n.placement,N.isOpen&&S()}))}function A(){R.length&&(angular.forEach(R,function(a){a()}),R.length=0)}function B(a){N&&N.isOpen&&D&&(b[0].contains(a.target)||D[0].contains(a.target)||q())}function C(){var a=d[k+"Trigger"];T(),L=p(a),"none"!==L.show&&L.show.forEach(function(a,c){"outsideClick"===a?(b.on("click",j),h.on("click",B)):a===L.hide[c]?b.on(a,j):a&&(b.on(a,m),b.on(L.hide[c],q)),b.on("keypress",function(a){27===a.which&&q()})})}var D,E,F,G,H,I,J,K=angular.isDefined(n.appendToBody)?n.appendToBody:!1,L=p(void 0),M=angular.isDefined(d[k+"Enable"]),N=a.$new(!0),O=!1,P=angular.isDefined(d[k+"IsOpen"])?l(d[k+"IsOpen"]):!1,Q=n.useContentExp?l(d[e]):!1,R=[],S=function(){D&&D.html()&&(I||(I=g(function(){var a=i.positionElements(b,D,N.placement,K);D.css({top:a.top+"px",left:a.left+"px"}),D.hasClass(a.placement.split("-")[0])||(D.removeClass(J.split("-")[0]),D.addClass(a.placement.split("-")[0])),D.hasClass(n.placementClassPrefix+a.placement)||(D.removeClass(n.placementClassPrefix+J),D.addClass(n.placementClassPrefix+a.placement)),D.hasClass("uib-position-measure")?(i.positionArrow(D,a.placement),D.removeClass("uib-position-measure")):J!==a.placement&&i.positionArrow(D,a.placement),J=a.placement,I=null},0,!1)))};N.origScope=a,N.isOpen=!1,o.add(N,{close:t}),N.contentExp=function(){return N.content},d.$observe("disabled",function(a){a&&s(),a&&N.isOpen&&t()}),P&&a.$watch(P,function(a){N&&!a===N.isOpen&&j()});var T=function(){L.show.forEach(function(a){"outsideClick"===a?b.off("click",j):(b.off(a,m),b.off(a,j))}),L.hide.forEach(function(a){"outsideClick"===a?h.off("click",B):b.off(a,q)})};C();var U=a.$eval(d[k+"Animation"]);N.animation=angular.isDefined(U)?!!U:n.animation;var V,W=k+"AppendToBody";V=W in d&&void 0===d[W]?!0:a.$eval(d[W]),K=angular.isDefined(V)?V:K,a.$on("$destroy",function(){T(),w(),o.remove(N),N=null})}}}}}]}).directive("uibTooltipTemplateTransclude",["$animate","$sce","$compile","$templateRequest",function(a,b,c,d){return{link:function(e,f,g){var h,i,j,k=e.$eval(g.tooltipTemplateTranscludeScope),l=0,m=function(){i&&(i.remove(),i=null),h&&(h.$destroy(),h=null),j&&(a.leave(j).then(function(){i=null}),i=j,j=null)};e.$watch(b.parseAsResourceUrl(g.uibTooltipTemplateTransclude),function(b){var g=++l;b?(d(b,!0).then(function(d){if(g===l){var e=k.$new(),i=d,n=c(i)(e,function(b){m(),a.enter(b,f)});h=e,j=n,h.$emit("$includeContentLoaded",b)}},function(){g===l&&(m(),e.$emit("$includeContentError",b))}),e.$emit("$includeContentRequested",b)):m()}),e.$on("$destroy",m)}}}]).directive("uibTooltipClasses",["$uibPosition",function(a){return{restrict:"A",link:function(b,c,d){if(b.placement){var e=a.parsePlacement(b.placement);c.addClass(e[0])}b.popupClass&&c.addClass(b.popupClass),b.animation()&&c.addClass(d.tooltipAnimationClass)}}}]).directive("uibTooltipPopup",function(){return{replace:!0,scope:{content:"@",placement:"@",popupClass:"@",animation:"&",isOpen:"&"},templateUrl:"uib/template/tooltip/tooltip-popup.html"}}).directive("uibTooltip",["$uibTooltip",function(a){return a("uibTooltip","tooltip","mouseenter")}]).directive("uibTooltipTemplatePopup",function(){return{replace:!0,scope:{contentExp:"&",placement:"@",popupClass:"@",animation:"&",isOpen:"&",originScope:"&"},templateUrl:"uib/template/tooltip/tooltip-template-popup.html"}}).directive("uibTooltipTemplate",["$uibTooltip",function(a){return a("uibTooltipTemplate","tooltip","mouseenter",{useContentExp:!0})}]).directive("uibTooltipHtmlPopup",function(){return{replace:!0,scope:{contentExp:"&",placement:"@",popupClass:"@",animation:"&",isOpen:"&"},templateUrl:"uib/template/tooltip/tooltip-html-popup.html"}}).directive("uibTooltipHtml",["$uibTooltip",function(a){return a("uibTooltipHtml","tooltip","mouseenter",{useContentExp:!0})}]),angular.module("ui.bootstrap.popover",["ui.bootstrap.tooltip"]).directive("uibPopoverTemplatePopup",function(){return{replace:!0,scope:{uibTitle:"@",contentExp:"&",placement:"@",popupClass:"@",animation:"&",isOpen:"&",originScope:"&"},templateUrl:"uib/template/popover/popover-template.html"}}).directive("uibPopoverTemplate",["$uibTooltip",function(a){return a("uibPopoverTemplate","popover","click",{useContentExp:!0})}]).directive("uibPopoverHtmlPopup",function(){return{replace:!0,scope:{contentExp:"&",uibTitle:"@",placement:"@",popupClass:"@",animation:"&",isOpen:"&"},templateUrl:"uib/template/popover/popover-html.html"}}).directive("uibPopoverHtml",["$uibTooltip",function(a){return a("uibPopoverHtml","popover","click",{useContentExp:!0})}]).directive("uibPopoverPopup",function(){return{replace:!0,scope:{uibTitle:"@",content:"@",placement:"@",popupClass:"@",animation:"&",isOpen:"&"},templateUrl:"uib/template/popover/popover.html"}}).directive("uibPopover",["$uibTooltip",function(a){return a("uibPopover","popover","click")}]),angular.module("ui.bootstrap.progressbar",[]).constant("uibProgressConfig",{animate:!0,max:100}).controller("UibProgressController",["$scope","$attrs","uibProgressConfig",function(a,b,c){function d(){return angular.isDefined(a.maxParam)?a.maxParam:c.max}var e=this,f=angular.isDefined(b.animate)?a.$parent.$eval(b.animate):c.animate;this.bars=[],a.max=d(),this.addBar=function(a,b,c){f||b.css({transition:"none"}),this.bars.push(a),a.max=d(),a.title=c&&angular.isDefined(c.title)?c.title:"progressbar",a.$watch("value",function(b){a.recalculatePercentage()}),a.recalculatePercentage=function(){var b=e.bars.reduce(function(a,b){return b.percent=+(100*b.value/b.max).toFixed(2),a+b.percent},0);b>100&&(a.percent-=b-100)},a.$on("$destroy",function(){b=null,e.removeBar(a)})},this.removeBar=function(a){this.bars.splice(this.bars.indexOf(a),1),this.bars.forEach(function(a){a.recalculatePercentage()})},a.$watch("maxParam",function(a){e.bars.forEach(function(a){a.max=d(),a.recalculatePercentage()})})}]).directive("uibProgress",function(){return{replace:!0,transclude:!0,controller:"UibProgressController",require:"uibProgress",scope:{maxParam:"=?max"},templateUrl:"uib/template/progressbar/progress.html"}}).directive("uibBar",function(){return{replace:!0,transclude:!0,require:"^uibProgress",scope:{value:"=",type:"@"},templateUrl:"uib/template/progressbar/bar.html",link:function(a,b,c,d){d.addBar(a,b,c)}}}).directive("uibProgressbar",function(){return{replace:!0,transclude:!0,controller:"UibProgressController",scope:{value:"=",maxParam:"=?max",type:"@"},templateUrl:"uib/template/progressbar/progressbar.html",link:function(a,b,c,d){d.addBar(a,angular.element(b.children()[0]),{title:c.title})}}}),angular.module("ui.bootstrap.rating",[]).constant("uibRatingConfig",{max:5,stateOn:null,stateOff:null,enableReset:!0,titles:["one","two","three","four","five"]}).controller("UibRatingController",["$scope","$attrs","uibRatingConfig",function(a,b,c){var d={$setViewValue:angular.noop},e=this;this.init=function(e){d=e,d.$render=this.render,d.$formatters.push(function(a){return angular.isNumber(a)&&a<<0!==a&&(a=Math.round(a)),a}),this.stateOn=angular.isDefined(b.stateOn)?a.$parent.$eval(b.stateOn):c.stateOn,this.stateOff=angular.isDefined(b.stateOff)?a.$parent.$eval(b.stateOff):c.stateOff,this.enableReset=angular.isDefined(b.enableReset)?a.$parent.$eval(b.enableReset):c.enableReset;var f=angular.isDefined(b.titles)?a.$parent.$eval(b.titles):c.titles;this.titles=angular.isArray(f)&&f.length>0?f:c.titles;var g=angular.isDefined(b.ratingStates)?a.$parent.$eval(b.ratingStates):new Array(angular.isDefined(b.max)?a.$parent.$eval(b.max):c.max);a.range=this.buildTemplateObjects(g)},this.buildTemplateObjects=function(a){for(var b=0,c=a.length;c>b;b++)a[b]=angular.extend({index:b},{stateOn:this.stateOn,stateOff:this.stateOff,title:this.getTitle(b)},a[b]);return a},this.getTitle=function(a){return a>=this.titles.length?a+1:this.titles[a]},a.rate=function(b){if(!a.readonly&&b>=0&&b<=a.range.length){var c=e.enableReset&&d.$viewValue===b?0:b;d.$setViewValue(c),d.$render()}},a.enter=function(b){a.readonly||(a.value=b),a.onHover({value:b})},a.reset=function(){a.value=d.$viewValue,a.onLeave()},a.onKeydown=function(b){/(37|38|39|40)/.test(b.which)&&(b.preventDefault(),b.stopPropagation(),a.rate(a.value+(38===b.which||39===b.which?1:-1)))},this.render=function(){a.value=d.$viewValue,a.title=e.getTitle(a.value-1)}}]).directive("uibRating",function(){return{require:["uibRating","ngModel"],scope:{readonly:"=?readOnly",onHover:"&",onLeave:"&"},controller:"UibRatingController",templateUrl:"uib/template/rating/rating.html",replace:!0,link:function(a,b,c,d){var e=d[0],f=d[1];e.init(f)}}}),angular.module("ui.bootstrap.tabs",[]).controller("UibTabsetController",["$scope",function(a){function b(a){for(var b=0;bb.index?1:a.index0&&13>b:b>=0&&24>b;return c&&""!==a.hours?(a.showMeridian&&(12===b&&(b=0),a.meridian===v[1]&&(b+=12)),b):void 0}function i(){var b=+a.minutes,c=b>=0&&60>b;return c&&""!==a.minutes?b:void 0}function j(){var b=+a.seconds;return b>=0&&60>b?b:void 0}function k(a,b){return null===a?"":angular.isDefined(a)&&a.toString().length<2&&!b?"0"+a:a.toString()}function l(a){m(),u.$setViewValue(new Date(s)),n(a)}function m(){u.$setValidity("time",!0),a.invalidHours=!1,a.invalidMinutes=!1,a.invalidSeconds=!1}function n(b){if(u.$modelValue){var c=s.getHours(),d=s.getMinutes(),e=s.getSeconds();a.showMeridian&&(c=0===c||12===c?12:c%12),a.hours="h"===b?c:k(c,!w),"m"!==b&&(a.minutes=k(d)),a.meridian=s.getHours()<12?v[0]:v[1],"s"!==b&&(a.seconds=k(e)),a.meridian=s.getHours()<12?v[0]:v[1]}else a.hours=null,a.minutes=null,a.seconds=null,a.meridian=v[0]}function o(a){s=q(s,a),l()}function p(a,b){return q(a,60*b)}function q(a,b){var c=new Date(a.getTime()+1e3*b),d=new Date(a);return d.setHours(c.getHours(),c.getMinutes(),c.getSeconds()),d}function r(){return(null===a.hours||""===a.hours)&&(null===a.minutes||""===a.minutes)&&(!a.showSeconds||a.showSeconds&&(null===a.seconds||""===a.seconds))}var s=new Date,t=[],u={$setViewValue:angular.noop},v=angular.isDefined(c.meridians)?a.$parent.$eval(c.meridians):g.meridians||f.DATETIME_FORMATS.AMPMS,w=angular.isDefined(c.padHours)?a.$parent.$eval(c.padHours):!0;a.tabindex=angular.isDefined(c.tabindex)?c.tabindex:0,b.removeAttr("tabindex"),this.init=function(b,d){u=b,u.$render=this.render,u.$formatters.unshift(function(a){return a?new Date(a):null});var e=d.eq(0),f=d.eq(1),h=d.eq(2),i=angular.isDefined(c.mousewheel)?a.$parent.$eval(c.mousewheel):g.mousewheel;i&&this.setupMousewheelEvents(e,f,h);var j=angular.isDefined(c.arrowkeys)?a.$parent.$eval(c.arrowkeys):g.arrowkeys;j&&this.setupArrowkeyEvents(e,f,h),a.readonlyInput=angular.isDefined(c.readonlyInput)?a.$parent.$eval(c.readonlyInput):g.readonlyInput,this.setupInputEvents(e,f,h)};var x=g.hourStep;c.hourStep&&t.push(a.$parent.$watch(d(c.hourStep),function(a){x=+a}));var y=g.minuteStep;c.minuteStep&&t.push(a.$parent.$watch(d(c.minuteStep),function(a){y=+a}));var z;t.push(a.$parent.$watch(d(c.min),function(a){var b=new Date(a);z=isNaN(b)?void 0:b}));var A;t.push(a.$parent.$watch(d(c.max),function(a){var b=new Date(a);A=isNaN(b)?void 0:b}));var B=!1;c.ngDisabled&&t.push(a.$parent.$watch(d(c.ngDisabled),function(a){B=a})),a.noIncrementHours=function(){var a=p(s,60*x);return B||a>A||s>a&&z>a},a.noDecrementHours=function(){var a=p(s,60*-x);return B||z>a||a>s&&a>A},a.noIncrementMinutes=function(){var a=p(s,y);return B||a>A||s>a&&z>a},a.noDecrementMinutes=function(){var a=p(s,-y);return B||z>a||a>s&&a>A},a.noIncrementSeconds=function(){var a=q(s,C);return B||a>A||s>a&&z>a},a.noDecrementSeconds=function(){var a=q(s,-C);return B||z>a||a>s&&a>A},a.noToggleMeridian=function(){return s.getHours()<12?B||p(s,720)>A:B||p(s,-720)0};b.bind("mousewheel wheel",function(b){B||a.$apply(e(b)?a.incrementHours():a.decrementHours()),b.preventDefault()}),c.bind("mousewheel wheel",function(b){B||a.$apply(e(b)?a.incrementMinutes():a.decrementMinutes()),b.preventDefault()}),d.bind("mousewheel wheel",function(b){B||a.$apply(e(b)?a.incrementSeconds():a.decrementSeconds()),b.preventDefault()})},this.setupArrowkeyEvents=function(b,c,d){b.bind("keydown",function(b){B||(38===b.which?(b.preventDefault(),a.incrementHours(),a.$apply()):40===b.which&&(b.preventDefault(),a.decrementHours(),a.$apply()))}),c.bind("keydown",function(b){B||(38===b.which?(b.preventDefault(),a.incrementMinutes(),a.$apply()):40===b.which&&(b.preventDefault(),a.decrementMinutes(),a.$apply()))}),d.bind("keydown",function(b){B||(38===b.which?(b.preventDefault(),a.incrementSeconds(),a.$apply()):40===b.which&&(b.preventDefault(),a.decrementSeconds(),a.$apply()))})},this.setupInputEvents=function(b,c,d){if(a.readonlyInput)return a.updateHours=angular.noop,a.updateMinutes=angular.noop,void(a.updateSeconds=angular.noop);var e=function(b,c,d){u.$setViewValue(null),u.$setValidity("time",!1),angular.isDefined(b)&&(a.invalidHours=b),angular.isDefined(c)&&(a.invalidMinutes=c),angular.isDefined(d)&&(a.invalidSeconds=d)};a.updateHours=function(){var a=h(),b=i();u.$setDirty(),angular.isDefined(a)&&angular.isDefined(b)?(s.setHours(a),s.setMinutes(b),z>s||s>A?e(!0):l("h")):e(!0)},b.bind("blur",function(b){u.$setTouched(),r()?m():null===a.hours||""===a.hours?e(!0):!a.invalidHours&&a.hours<10&&a.$apply(function(){a.hours=k(a.hours,!w)})}),a.updateMinutes=function(){var a=i(),b=h();u.$setDirty(),angular.isDefined(a)&&angular.isDefined(b)?(s.setHours(b),s.setMinutes(a),z>s||s>A?e(void 0,!0):l("m")):e(void 0,!0)},c.bind("blur",function(b){u.$setTouched(),r()?m():null===a.minutes?e(void 0,!0):!a.invalidMinutes&&a.minutes<10&&a.$apply(function(){a.minutes=k(a.minutes)})}),a.updateSeconds=function(){var a=j();u.$setDirty(),angular.isDefined(a)?(s.setSeconds(a),l("s")):e(void 0,void 0,!0)},d.bind("blur",function(b){r()?m():!a.invalidSeconds&&a.seconds<10&&a.$apply(function(){a.seconds=k(a.seconds)})})},this.render=function(){var b=u.$viewValue;isNaN(b)?(u.$setValidity("time",!1),e.error('Timepicker directive: "ng-model" value must be a Date object, a number of milliseconds since 01.01.1970 or a string representing an RFC2822 or ISO 8601 date.')):(b&&(s=b),z>s||s>A?(u.$setValidity("time",!1),a.invalidHours=!0,a.invalidMinutes=!0):m(),n())},a.showSpinners=angular.isDefined(c.showSpinners)?a.$parent.$eval(c.showSpinners):g.showSpinners,a.incrementHours=function(){a.noIncrementHours()||o(60*x*60)},a.decrementHours=function(){a.noDecrementHours()||o(60*-x*60)},a.incrementMinutes=function(){a.noIncrementMinutes()||o(60*y)},a.decrementMinutes=function(){a.noDecrementMinutes()||o(60*-y)},a.incrementSeconds=function(){a.noIncrementSeconds()||o(C)},a.decrementSeconds=function(){a.noDecrementSeconds()||o(-C)},a.toggleMeridian=function(){var b=i(),c=h();a.noToggleMeridian()||(angular.isDefined(b)&&angular.isDefined(c)?o(720*(s.getHours()<12?60:-60)):a.meridian=a.meridian===v[0]?v[1]:v[0])},a.blur=function(){u.$setTouched()},a.$on("$destroy",function(){for(;t.length;)t.shift()()})}]).directive("uibTimepicker",["uibTimepickerConfig",function(a){return{require:["uibTimepicker","?^ngModel"],controller:"UibTimepickerController",controllerAs:"timepicker",replace:!0,scope:{},templateUrl:function(b,c){return c.templateUrl||a.templateUrl},link:function(a,b,c,d){var e=d[0],f=d[1];f&&e.init(f,b.find("input"))}}}]),angular.module("ui.bootstrap.typeahead",["ui.bootstrap.debounce","ui.bootstrap.position"]).factory("uibTypeaheadParser",["$parse",function(a){var b=/^\s*([\s\S]+?)(?:\s+as\s+([\s\S]+?))?\s+for\s+(?:([\$\w][\$\w\d]*))\s+in\s+([\s\S]+?)$/;return{parse:function(c){var d=c.match(b);if(!d)throw new Error('Expected typeahead specification in form of "_modelValue_ (as _label_)? for _item_ in _collection_" but got "'+c+'".');return{itemName:d[3],source:a(d[4]),viewMapper:a(d[2]||d[1]),modelMapper:a(d[1])}}}}]).controller("UibTypeaheadController",["$scope","$element","$attrs","$compile","$parse","$q","$timeout","$document","$window","$rootScope","$$debounce","$uibPosition","uibTypeaheadParser",function(a,b,c,d,e,f,g,h,i,j,k,l,m){function n(){N.moveInProgress||(N.moveInProgress=!0,N.$digest()),Y()}function o(){N.position=D?l.offset(b):l.position(b),N.position.top+=b.prop("offsetHeight")}var p,q,r=[9,13,27,38,40],s=200,t=a.$eval(c.typeaheadMinLength);t||0===t||(t=1),a.$watch(c.typeaheadMinLength,function(a){t=a||0===a?a:1});var u=a.$eval(c.typeaheadWaitMs)||0,v=a.$eval(c.typeaheadEditable)!==!1;a.$watch(c.typeaheadEditable,function(a){v=a!==!1});var w,x,y=e(c.typeaheadLoading).assign||angular.noop,z=e(c.typeaheadOnSelect),A=angular.isDefined(c.typeaheadSelectOnBlur)?a.$eval(c.typeaheadSelectOnBlur):!1,B=e(c.typeaheadNoResults).assign||angular.noop,C=c.typeaheadInputFormatter?e(c.typeaheadInputFormatter):void 0,D=c.typeaheadAppendToBody?a.$eval(c.typeaheadAppendToBody):!1,E=c.typeaheadAppendTo?a.$eval(c.typeaheadAppendTo):null,F=a.$eval(c.typeaheadFocusFirst)!==!1,G=c.typeaheadSelectOnExact?a.$eval(c.typeaheadSelectOnExact):!1,H=e(c.typeaheadIsOpen).assign||angular.noop,I=a.$eval(c.typeaheadShowHint)||!1,J=e(c.ngModel),K=e(c.ngModel+"($$$p)"),L=function(b,c){return angular.isFunction(J(a))&&q&&q.$options&&q.$options.getterSetter?K(b,{$$$p:c}):J.assign(b,c)},M=m.parse(c.uibTypeahead),N=a.$new(),O=a.$on("$destroy",function(){N.$destroy()});N.$on("$destroy",O);var P="typeahead-"+N.$id+"-"+Math.floor(1e4*Math.random());b.attr({"aria-autocomplete":"list","aria-expanded":!1,"aria-owns":P});var Q,R;I&&(Q=angular.element("
        "),Q.css("position","relative"),b.after(Q),R=b.clone(),R.attr("placeholder",""),R.attr("tabindex","-1"),R.val(""),R.css({position:"absolute",top:"0px",left:"0px","border-color":"transparent","box-shadow":"none",opacity:1,background:"none 0% 0% / auto repeat scroll padding-box border-box rgb(255, 255, 255)",color:"#999"}),b.css({position:"relative","vertical-align":"top","background-color":"transparent"}),Q.append(R),R.after(b));var S=angular.element("
        ");S.attr({id:P,matches:"matches",active:"activeIdx",select:"select(activeIdx, evt)","move-in-progress":"moveInProgress",query:"query",position:"position","assign-is-open":"assignIsOpen(isOpen)",debounce:"debounceUpdate"}),angular.isDefined(c.typeaheadTemplateUrl)&&S.attr("template-url",c.typeaheadTemplateUrl),angular.isDefined(c.typeaheadPopupTemplateUrl)&&S.attr("popup-template-url",c.typeaheadPopupTemplateUrl);var T=function(){I&&R.val("")},U=function(){N.matches=[],N.activeIdx=-1,b.attr("aria-expanded",!1),T()},V=function(a){return P+"-option-"+a};N.$watch("activeIdx",function(a){0>a?b.removeAttr("aria-activedescendant"):b.attr("aria-activedescendant",V(a))});var W=function(a,b){return N.matches.length>b&&a?a.toUpperCase()===N.matches[b].label.toUpperCase():!1},X=function(c,d){var e={$viewValue:c};y(a,!0),B(a,!1),f.when(M.source(a,e)).then(function(f){var g=c===p.$viewValue;if(g&&w)if(f&&f.length>0){N.activeIdx=F?0:-1,B(a,!1),N.matches.length=0;for(var h=0;h0&&i.slice(0,c.length).toUpperCase()===c.toUpperCase()?R.val(c+i.slice(c.length)):R.val("")}}else U(),B(a,!0);g&&y(a,!1)},function(){U(),y(a,!1),B(a,!0)})};D&&(angular.element(i).on("resize",n),h.find("body").on("scroll",n));var Y=k(function(){N.matches.length&&o(),N.moveInProgress=!1},s);N.moveInProgress=!1,N.query=void 0;var Z,$=function(a){Z=g(function(){X(a)},u)},_=function(){Z&&g.cancel(Z)};U(),N.assignIsOpen=function(b){H(a,b)},N.select=function(d,e){var f,h,i={};x=!0,i[M.itemName]=h=N.matches[d].model,f=M.modelMapper(a,i),L(a,f),p.$setValidity("editable",!0),p.$setValidity("parse",!0),z(a,{$item:h,$model:f,$label:M.viewMapper(a,i),$event:e}),U(),N.$eval(c.typeaheadFocusOnSelect)!==!1&&g(function(){b[0].focus()},0,!1)},b.on("keydown",function(b){if(0!==N.matches.length&&-1!==r.indexOf(b.which)){if(-1===N.activeIdx&&(9===b.which||13===b.which)||9===b.which&&b.shiftKey)return U(),void N.$digest();b.preventDefault();var c;switch(b.which){case 9:case 13:N.$apply(function(){angular.isNumber(N.debounceUpdate)||angular.isObject(N.debounceUpdate)?k(function(){N.select(N.activeIdx,b)},angular.isNumber(N.debounceUpdate)?N.debounceUpdate:N.debounceUpdate["default"]):N.select(N.activeIdx,b)});break;case 27:b.stopPropagation(),U(),a.$digest();break;case 38:N.activeIdx=(N.activeIdx>0?N.activeIdx:N.matches.length)-1,N.$digest(),c=S.find("li")[N.activeIdx],c.parentNode.scrollTop=c.offsetTop;break;case 40:N.activeIdx=(N.activeIdx+1)%N.matches.length,N.$digest(),c=S.find("li")[N.activeIdx],c.parentNode.scrollTop=c.offsetTop}}}),b.bind("focus",function(a){w=!0,0!==t||p.$viewValue||g(function(){X(p.$viewValue,a)},0)}),b.bind("blur",function(a){A&&N.matches.length&&-1!==N.activeIdx&&!x&&(x=!0,N.$apply(function(){angular.isObject(N.debounceUpdate)&&angular.isNumber(N.debounceUpdate.blur)?k(function(){N.select(N.activeIdx,a)},N.debounceUpdate.blur):N.select(N.activeIdx,a)})),!v&&p.$error.editable&&(p.$setViewValue(),p.$setValidity("editable",!0),p.$setValidity("parse",!0),b.val("")),w=!1,x=!1});var aa=function(c){b[0]!==c.target&&3!==c.which&&0!==N.matches.length&&(U(),j.$$phase||a.$digest())};h.on("click",aa),a.$on("$destroy",function(){h.off("click",aa),(D||E)&&ba.remove(),D&&(angular.element(i).off("resize",n),h.find("body").off("scroll",n)),S.remove(),I&&Q.remove()});var ba=d(S)(N);D?h.find("body").append(ba):E?angular.element(E).eq(0).append(ba):b.after(ba),this.init=function(b,c){p=b,q=c,N.debounceUpdate=p.$options&&e(p.$options.debounce)(a),p.$parsers.unshift(function(b){return w=!0,0===t||b&&b.length>=t?u>0?(_(),$(b)):X(b):(y(a,!1),_(),U()),v?b:b?void p.$setValidity("editable",!1):(p.$setValidity("editable",!0),null)}),p.$formatters.push(function(b){var c,d,e={};return v||p.$setValidity("editable",!0),C?(e.$model=b,C(a,e)):(e[M.itemName]=b,c=M.viewMapper(a,e),e[M.itemName]=void 0,d=M.viewMapper(a,e),c!==d?c:b)})}}]).directive("uibTypeahead",function(){return{controller:"UibTypeaheadController",require:["ngModel","^?ngModelOptions","uibTypeahead"],link:function(a,b,c,d){d[2].init(d[0],d[1])}}}).directive("uibTypeaheadPopup",["$$debounce",function(a){return{scope:{matches:"=",query:"=",active:"=",position:"&",moveInProgress:"=",select:"&",assignIsOpen:"&",debounce:"&"},replace:!0,templateUrl:function(a,b){return b.popupTemplateUrl||"uib/template/typeahead/typeahead-popup.html"},link:function(b,c,d){b.templateUrl=d.templateUrl,b.isOpen=function(){var a=b.matches.length>0;return b.assignIsOpen({isOpen:a}),a},b.isActive=function(a){return b.active===a},b.selectActive=function(a){b.active=a},b.selectMatch=function(c,d){var e=b.debounce();angular.isNumber(e)||angular.isObject(e)?a(function(){b.select({activeIdx:c,evt:d})},angular.isNumber(e)?e:e["default"]):b.select({activeIdx:c,evt:d})}}}}]).directive("uibTypeaheadMatch",["$templateRequest","$compile","$parse",function(a,b,c){return{scope:{index:"=",match:"=",query:"="},link:function(d,e,f){var g=c(f.templateUrl)(d.$parent)||"uib/template/typeahead/typeahead-match.html";a(g).then(function(a){var c=angular.element(a.trim());e.replaceWith(c),b(c)(d)})}}}]).filter("uibTypeaheadHighlight",["$sce","$injector","$log",function(a,b,c){function d(a){return a.replace(/([.?*+^$[\]\\(){}|-])/g,"\\$1")}function e(a){return/<.*>/g.test(a)}var f;return f=b.has("$sanitize"),function(b,g){return!f&&e(b)&&c.warn("Unsafe use of typeahead please use ngSanitize"),b=g?(""+b).replace(new RegExp(d(g),"gi"),"$&"):b,f||(b=a.trustAsHtml(b)),b}}]),angular.module("uib/template/accordion/accordion-group.html",[]).run(["$templateCache",function(a){a.put("uib/template/accordion/accordion-group.html",'
        \n \n
        \n
        \n
        \n
        \n')}]),angular.module("uib/template/accordion/accordion.html",[]).run(["$templateCache",function(a){a.put("uib/template/accordion/accordion.html",'
        ')}]),angular.module("uib/template/alert/alert.html",[]).run(["$templateCache",function(a){a.put("uib/template/alert/alert.html",'\n')}]),angular.module("uib/template/carousel/carousel.html",[]).run(["$templateCache",function(a){a.put("uib/template/carousel/carousel.html",'\n'); +}]),angular.module("uib/template/carousel/slide.html",[]).run(["$templateCache",function(a){a.put("uib/template/carousel/slide.html",'
        \n')}]),angular.module("uib/template/datepicker/datepicker.html",[]).run(["$templateCache",function(a){a.put("uib/template/datepicker/datepicker.html",'
        \n \n \n \n
        \n')}]),angular.module("uib/template/datepicker/day.html",[]).run(["$templateCache",function(a){a.put("uib/template/datepicker/day.html",'\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
        {{::label.abbr}}
        {{ weekNumbers[$index] }}\n \n
        \n')}]),angular.module("uib/template/datepicker/month.html",[]).run(["$templateCache",function(a){a.put("uib/template/datepicker/month.html",'\n \n \n \n \n \n \n \n \n \n \n \n \n
        \n \n
        \n')}]),angular.module("uib/template/datepicker/year.html",[]).run(["$templateCache",function(a){a.put("uib/template/datepicker/year.html",'\n \n \n \n \n \n \n \n \n \n \n \n \n
        \n \n
        \n')}]),angular.module("uib/template/datepickerPopup/popup.html",[]).run(["$templateCache",function(a){a.put("uib/template/datepickerPopup/popup.html",'
        \n \n
        \n')}]),angular.module("uib/template/modal/backdrop.html",[]).run(["$templateCache",function(a){a.put("uib/template/modal/backdrop.html",'\n')}]),angular.module("uib/template/modal/window.html",[]).run(["$templateCache",function(a){a.put("uib/template/modal/window.html",'\n')}]),angular.module("uib/template/pager/pager.html",[]).run(["$templateCache",function(a){a.put("uib/template/pager/pager.html",'\n')}]),angular.module("uib/template/pagination/pagination.html",[]).run(["$templateCache",function(a){a.put("uib/template/pagination/pagination.html",'\n')}]),angular.module("uib/template/tooltip/tooltip-html-popup.html",[]).run(["$templateCache",function(a){a.put("uib/template/tooltip/tooltip-html-popup.html",'
        \n
        \n
        \n
        \n')}]),angular.module("uib/template/tooltip/tooltip-popup.html",[]).run(["$templateCache",function(a){a.put("uib/template/tooltip/tooltip-popup.html",'
        \n
        \n
        \n
        \n')}]),angular.module("uib/template/tooltip/tooltip-template-popup.html",[]).run(["$templateCache",function(a){a.put("uib/template/tooltip/tooltip-template-popup.html",'
        \n
        \n
        \n
        \n')}]),angular.module("uib/template/popover/popover-html.html",[]).run(["$templateCache",function(a){a.put("uib/template/popover/popover-html.html",'
        \n
        \n\n
        \n

        \n
        \n
        \n
        \n')}]),angular.module("uib/template/popover/popover-template.html",[]).run(["$templateCache",function(a){a.put("uib/template/popover/popover-template.html",'
        \n
        \n\n
        \n

        \n
        \n
        \n
        \n')}]),angular.module("uib/template/popover/popover.html",[]).run(["$templateCache",function(a){a.put("uib/template/popover/popover.html",'
        \n
        \n\n
        \n

        \n
        \n
        \n
        \n')}]),angular.module("uib/template/progressbar/bar.html",[]).run(["$templateCache",function(a){a.put("uib/template/progressbar/bar.html",'
        \n')}]),angular.module("uib/template/progressbar/progress.html",[]).run(["$templateCache",function(a){a.put("uib/template/progressbar/progress.html",'
        ')}]),angular.module("uib/template/progressbar/progressbar.html",[]).run(["$templateCache",function(a){a.put("uib/template/progressbar/progressbar.html",'
        \n
        \n
        \n')}]),angular.module("uib/template/rating/rating.html",[]).run(["$templateCache",function(a){a.put("uib/template/rating/rating.html",'\n ({{ $index < value ? \'*\' : \' \' }})\n \n\n')}]),angular.module("uib/template/tabs/tab.html",[]).run(["$templateCache",function(a){a.put("uib/template/tabs/tab.html",'\n')}]),angular.module("uib/template/tabs/tabset.html",[]).run(["$templateCache",function(a){a.put("uib/template/tabs/tabset.html",'
        \n \n
        \n
        \n
        \n
        \n
        \n')}]),angular.module("uib/template/timepicker/timepicker.html",[]).run(["$templateCache",function(a){a.put("uib/template/timepicker/timepicker.html",'\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
          
        \n \n :\n \n :\n \n
          
        \n')}]),angular.module("uib/template/typeahead/typeahead-match.html",[]).run(["$templateCache",function(a){a.put("uib/template/typeahead/typeahead-match.html",'\n')}]),angular.module("uib/template/typeahead/typeahead-popup.html",[]).run(["$templateCache",function(a){a.put("uib/template/typeahead/typeahead-popup.html",'\n')}]),angular.module("ui.bootstrap.carousel").run(function(){!angular.$$csp().noInlineStyle&&!angular.$$uibCarouselCss&&angular.element(document).find("head").prepend(''),angular.$$uibCarouselCss=!0}),angular.module("ui.bootstrap.datepicker").run(function(){!angular.$$csp().noInlineStyle&&!angular.$$uibDatepickerCss&&angular.element(document).find("head").prepend(''),angular.$$uibDatepickerCss=!0}),angular.module("ui.bootstrap.position").run(function(){!angular.$$csp().noInlineStyle&&!angular.$$uibPositionCss&&angular.element(document).find("head").prepend(''),angular.$$uibPositionCss=!0}),angular.module("ui.bootstrap.datepickerPopup").run(function(){!angular.$$csp().noInlineStyle&&!angular.$$uibDatepickerpopupCss&&angular.element(document).find("head").prepend(''),angular.$$uibDatepickerpopupCss=!0}),angular.module("ui.bootstrap.tooltip").run(function(){!angular.$$csp().noInlineStyle&&!angular.$$uibTooltipCss&&angular.element(document).find("head").prepend(''),angular.$$uibTooltipCss=!0}),angular.module("ui.bootstrap.timepicker").run(function(){!angular.$$csp().noInlineStyle&&!angular.$$uibTimepickerCss&&angular.element(document).find("head").prepend(''),angular.$$uibTimepickerCss=!0}),angular.module("ui.bootstrap.typeahead").run(function(){!angular.$$csp().noInlineStyle&&!angular.$$uibTypeaheadCss&&angular.element(document).find("head").prepend(''),angular.$$uibTypeaheadCss=!0}); \ No newline at end of file diff --git a/app/bower_components/angular-bootstrap/ui-bootstrap.js b/app/bower_components/angular-bootstrap/ui-bootstrap.js index c93f0de..da83e4c 100644 --- a/app/bower_components/angular-bootstrap/ui-bootstrap.js +++ b/app/bower_components/angular-bootstrap/ui-bootstrap.js @@ -2,159 +2,101 @@ * angular-ui-bootstrap * http://angular-ui.github.io/bootstrap/ - * Version: 0.11.0 - 2014-05-01 + * Version: 1.3.2 - 2016-04-14 * License: MIT - */ -angular.module("ui.bootstrap", ["ui.bootstrap.transition","ui.bootstrap.collapse","ui.bootstrap.accordion","ui.bootstrap.alert","ui.bootstrap.bindHtml","ui.bootstrap.buttons","ui.bootstrap.carousel","ui.bootstrap.dateparser","ui.bootstrap.position","ui.bootstrap.datepicker","ui.bootstrap.dropdown","ui.bootstrap.modal","ui.bootstrap.pagination","ui.bootstrap.tooltip","ui.bootstrap.popover","ui.bootstrap.progressbar","ui.bootstrap.rating","ui.bootstrap.tabs","ui.bootstrap.timepicker","ui.bootstrap.typeahead"]); -angular.module('ui.bootstrap.transition', []) - -/** - * $transition service provides a consistent interface to trigger CSS 3 transitions and to be informed when they complete. - * @param {DOMElement} element The DOMElement that will be animated. - * @param {string|object|function} trigger The thing that will cause the transition to start: - * - As a string, it represents the css class to be added to the element. - * - As an object, it represents a hash of style attributes to be applied to the element. - * - As a function, it represents a function to be called that will cause the transition to occur. - * @return {Promise} A promise that is resolved when the transition finishes. - */ -.factory('$transition', ['$q', '$timeout', '$rootScope', function($q, $timeout, $rootScope) { - - var $transition = function(element, trigger, options) { - options = options || {}; - var deferred = $q.defer(); - var endEventName = $transition[options.animation ? 'animationEndEventName' : 'transitionEndEventName']; - - var transitionEndHandler = function(event) { - $rootScope.$apply(function() { - element.unbind(endEventName, transitionEndHandler); - deferred.resolve(element); - }); - }; - - if (endEventName) { - element.bind(endEventName, transitionEndHandler); - } - - // Wrap in a timeout to allow the browser time to update the DOM before the transition is to occur - $timeout(function() { - if ( angular.isString(trigger) ) { - element.addClass(trigger); - } else if ( angular.isFunction(trigger) ) { - trigger(element); - } else if ( angular.isObject(trigger) ) { - element.css(trigger); - } - //If browser does not support transitions, instantly resolve - if ( !endEventName ) { - deferred.resolve(element); - } - }); - - // Add our custom cancel function to the promise that is returned - // We can call this if we are about to run a new transition, which we know will prevent this transition from ending, - // i.e. it will therefore never raise a transitionEnd event for that transition - deferred.promise.cancel = function() { - if ( endEventName ) { - element.unbind(endEventName, transitionEndHandler); - } - deferred.reject('Transition cancelled'); - }; - - return deferred.promise; - }; - - // Work out the name of the transitionEnd event - var transElement = document.createElement('trans'); - var transitionEndEventNames = { - 'WebkitTransition': 'webkitTransitionEnd', - 'MozTransition': 'transitionend', - 'OTransition': 'oTransitionEnd', - 'transition': 'transitionend' - }; - var animationEndEventNames = { - 'WebkitTransition': 'webkitAnimationEnd', - 'MozTransition': 'animationend', - 'OTransition': 'oAnimationEnd', - 'transition': 'animationend' - }; - function findEndEventName(endEventNames) { - for (var name in endEventNames){ - if (transElement.style[name] !== undefined) { - return endEventNames[name]; - } - } - } - $transition.transitionEndEventName = findEndEventName(transitionEndEventNames); - $transition.animationEndEventName = findEndEventName(animationEndEventNames); - return $transition; -}]); - -angular.module('ui.bootstrap.collapse', ['ui.bootstrap.transition']) - - .directive('collapse', ['$transition', function ($transition) { + */angular.module("ui.bootstrap", ["ui.bootstrap.collapse","ui.bootstrap.accordion","ui.bootstrap.alert","ui.bootstrap.buttons","ui.bootstrap.carousel","ui.bootstrap.dateparser","ui.bootstrap.isClass","ui.bootstrap.datepicker","ui.bootstrap.position","ui.bootstrap.datepickerPopup","ui.bootstrap.debounce","ui.bootstrap.dropdown","ui.bootstrap.stackedMap","ui.bootstrap.modal","ui.bootstrap.paging","ui.bootstrap.pager","ui.bootstrap.pagination","ui.bootstrap.tooltip","ui.bootstrap.popover","ui.bootstrap.progressbar","ui.bootstrap.rating","ui.bootstrap.tabs","ui.bootstrap.timepicker","ui.bootstrap.typeahead"]); +angular.module('ui.bootstrap.collapse', []) + .directive('uibCollapse', ['$animate', '$q', '$parse', '$injector', function($animate, $q, $parse, $injector) { + var $animateCss = $injector.has('$animateCss') ? $injector.get('$animateCss') : null; return { - link: function (scope, element, attrs) { + link: function(scope, element, attrs) { + var expandingExpr = $parse(attrs.expanding), + expandedExpr = $parse(attrs.expanded), + collapsingExpr = $parse(attrs.collapsing), + collapsedExpr = $parse(attrs.collapsed); - var initialAnimSkip = true; - var currentTransition; - - function doTransition(change) { - var newTransition = $transition(element, change); - if (currentTransition) { - currentTransition.cancel(); - } - currentTransition = newTransition; - newTransition.then(newTransitionDone, newTransitionDone); - return newTransition; - - function newTransitionDone() { - // Make sure it's this transition, otherwise, leave it alone. - if (currentTransition === newTransition) { - currentTransition = undefined; - } - } + if (!scope.$eval(attrs.uibCollapse)) { + element.addClass('in') + .addClass('collapse') + .attr('aria-expanded', true) + .attr('aria-hidden', false) + .css({height: 'auto'}); } function expand() { - if (initialAnimSkip) { - initialAnimSkip = false; - expandDone(); - } else { - element.removeClass('collapse').addClass('collapsing'); - doTransition({ height: element[0].scrollHeight + 'px' }).then(expandDone); + if (element.hasClass('collapse') && element.hasClass('in')) { + return; } + + $q.resolve(expandingExpr(scope)) + .then(function() { + element.removeClass('collapse') + .addClass('collapsing') + .attr('aria-expanded', true) + .attr('aria-hidden', false); + + if ($animateCss) { + $animateCss(element, { + addClass: 'in', + easing: 'ease', + to: { height: element[0].scrollHeight + 'px' } + }).start()['finally'](expandDone); + } else { + $animate.addClass(element, 'in', { + to: { height: element[0].scrollHeight + 'px' } + }).then(expandDone); + } + }); } function expandDone() { - element.removeClass('collapsing'); - element.addClass('collapse in'); - element.css({height: 'auto'}); + element.removeClass('collapsing') + .addClass('collapse') + .css({height: 'auto'}); + expandedExpr(scope); } function collapse() { - if (initialAnimSkip) { - initialAnimSkip = false; - collapseDone(); - element.css({height: 0}); - } else { - // CSS transitions don't work with height: auto, so we have to manually change the height to a specific value - element.css({ height: element[0].scrollHeight + 'px' }); - //trigger reflow so a browser realizes that height was updated from auto to a specific value - var x = element[0].offsetWidth; - - element.removeClass('collapse in').addClass('collapsing'); - - doTransition({ height: 0 }).then(collapseDone); + if (!element.hasClass('collapse') && !element.hasClass('in')) { + return collapseDone(); } + + $q.resolve(collapsingExpr(scope)) + .then(function() { + element + // IMPORTANT: The height must be set before adding "collapsing" class. + // Otherwise, the browser attempts to animate from height 0 (in + // collapsing class) to the given height here. + .css({height: element[0].scrollHeight + 'px'}) + // initially all panel collapse have the collapse class, this removal + // prevents the animation from jumping to collapsed state + .removeClass('collapse') + .addClass('collapsing') + .attr('aria-expanded', false) + .attr('aria-hidden', true); + + if ($animateCss) { + $animateCss(element, { + removeClass: 'in', + to: {height: '0'} + }).start()['finally'](collapseDone); + } else { + $animate.removeClass(element, 'in', { + to: {height: '0'} + }).then(collapseDone); + } + }); } function collapseDone() { - element.removeClass('collapsing'); - element.addClass('collapse'); + element.css({height: '0'}); // Required so that collapse works when animation is disabled + element.removeClass('collapsing') + .addClass('collapse'); + collapsedExpr(scope); } - scope.$watch(attrs.collapse, function (shouldCollapse) { + scope.$watch(attrs.uibCollapse, function(shouldCollapse) { if (shouldCollapse) { collapse(); } else { @@ -167,21 +109,21 @@ angular.module('ui.bootstrap.collapse', ['ui.bootstrap.transition']) angular.module('ui.bootstrap.accordion', ['ui.bootstrap.collapse']) -.constant('accordionConfig', { +.constant('uibAccordionConfig', { closeOthers: true }) -.controller('AccordionController', ['$scope', '$attrs', 'accordionConfig', function ($scope, $attrs, accordionConfig) { - +.controller('UibAccordionController', ['$scope', '$attrs', 'uibAccordionConfig', function($scope, $attrs, accordionConfig) { // This array keeps track of the accordion groups this.groups = []; // Ensure that all the groups in this accordion are closed, unless close-others explicitly says not to this.closeOthers = function(openGroup) { - var closeOthers = angular.isDefined($attrs.closeOthers) ? $scope.$eval($attrs.closeOthers) : accordionConfig.closeOthers; - if ( closeOthers ) { - angular.forEach(this.groups, function (group) { - if ( group !== openGroup ) { + var closeOthers = angular.isDefined($attrs.closeOthers) ? + $scope.$eval($attrs.closeOthers) : accordionConfig.closeOthers; + if (closeOthers) { + angular.forEach(this.groups, function(group) { + if (group !== openGroup) { group.isOpen = false; } }); @@ -193,7 +135,7 @@ angular.module('ui.bootstrap.accordion', ['ui.bootstrap.collapse']) var that = this; this.groups.push(groupScope); - groupScope.$on('$destroy', function (event) { + groupScope.$on('$destroy', function(event) { that.removeGroup(groupScope); }); }; @@ -201,35 +143,37 @@ angular.module('ui.bootstrap.accordion', ['ui.bootstrap.collapse']) // This is called from the accordion-group directive when to remove itself this.removeGroup = function(group) { var index = this.groups.indexOf(group); - if ( index !== -1 ) { + if (index !== -1) { this.groups.splice(index, 1); } }; - }]) // The accordion directive simply sets up the directive controller // and adds an accordion CSS class to itself element. -.directive('accordion', function () { +.directive('uibAccordion', function() { return { - restrict:'EA', - controller:'AccordionController', + controller: 'UibAccordionController', + controllerAs: 'accordion', transclude: true, - replace: false, - templateUrl: 'template/accordion/accordion.html' + templateUrl: function(element, attrs) { + return attrs.templateUrl || 'uib/template/accordion/accordion.html'; + } }; }) // The accordion-group directive indicates a block of html that will expand and collapse in an accordion -.directive('accordionGroup', function() { +.directive('uibAccordionGroup', function() { return { - require:'^accordion', // We need this directive to be inside an accordion - restrict:'EA', - transclude:true, // It transcludes the contents of the directive into the template + require: '^uibAccordion', // We need this directive to be inside an accordion + transclude: true, // It transcludes the contents of the directive into the template replace: true, // The element containing the directive will be replaced with the template - templateUrl:'template/accordion/accordion-group.html', + templateUrl: function(element, attrs) { + return attrs.templateUrl || 'uib/template/accordion/accordion-group.html'; + }, scope: { heading: '@', // Interpolate the heading attribute onto this scope + panelClass: '@?', // Ditto with panelClass isOpen: '=?', isDisabled: '=?' }, @@ -241,55 +185,57 @@ angular.module('ui.bootstrap.accordion', ['ui.bootstrap.collapse']) link: function(scope, element, attrs, accordionCtrl) { accordionCtrl.addGroup(scope); + scope.openClass = attrs.openClass || 'panel-open'; + scope.panelClass = attrs.panelClass || 'panel-default'; scope.$watch('isOpen', function(value) { - if ( value ) { + element.toggleClass(scope.openClass, !!value); + if (value) { accordionCtrl.closeOthers(scope); } }); - scope.toggleOpen = function() { - if ( !scope.isDisabled ) { - scope.isOpen = !scope.isOpen; + scope.toggleOpen = function($event) { + if (!scope.isDisabled) { + if (!$event || $event.which === 32) { + scope.isOpen = !scope.isOpen; + } } }; + + var id = 'accordiongroup-' + scope.$id + '-' + Math.floor(Math.random() * 10000); + scope.headingId = id + '-tab'; + scope.panelId = id + '-panel'; } }; }) // Use accordion-heading below an accordion-group to provide a heading containing HTML -// -// Heading containing HTML - -// -.directive('accordionHeading', function() { +.directive('uibAccordionHeading', function() { return { - restrict: 'EA', transclude: true, // Grab the contents to be used as the heading template: '', // In effect remove this element! replace: true, - require: '^accordionGroup', - link: function(scope, element, attr, accordionGroupCtrl, transclude) { + require: '^uibAccordionGroup', + link: function(scope, element, attrs, accordionGroupCtrl, transclude) { // Pass the heading to the accordion-group controller // so that it can be transcluded into the right place in the template // [The second parameter to transclude causes the elements to be cloned so that they work in ng-repeat] - accordionGroupCtrl.setHeading(transclude(scope, function() {})); + accordionGroupCtrl.setHeading(transclude(scope, angular.noop)); } }; }) // Use in the accordion-group template to indicate where you want the heading to be transcluded // You must provide the property on the accordion-group controller that will hold the transcluded element -//
        -// -// ... -//
        -.directive('accordionTransclude', function() { +.directive('uibAccordionTransclude', function() { return { - require: '^accordionGroup', - link: function(scope, element, attr, controller) { - scope.$watch(function() { return controller[attr.accordionTransclude]; }, function(heading) { - if ( heading ) { - element.html(''); - element.append(heading); + require: '^uibAccordionGroup', + link: function(scope, element, attrs, controller) { + scope.$watch(function() { return controller[attrs.uibAccordionTransclude]; }, function(heading) { + if (heading) { + var elem = angular.element(element[0].querySelector('[uib-accordion-header]')); + elem.html(''); + elem.append(heading); } }); } @@ -298,17 +244,28 @@ angular.module('ui.bootstrap.accordion', ['ui.bootstrap.collapse']) angular.module('ui.bootstrap.alert', []) -.controller('AlertController', ['$scope', '$attrs', function ($scope, $attrs) { - $scope.closeable = 'close' in $attrs; +.controller('UibAlertController', ['$scope', '$attrs', '$interpolate', '$timeout', function($scope, $attrs, $interpolate, $timeout) { + $scope.closeable = !!$attrs.close; + + var dismissOnTimeout = angular.isDefined($attrs.dismissOnTimeout) ? + $interpolate($attrs.dismissOnTimeout)($scope.$parent) : null; + + if (dismissOnTimeout) { + $timeout(function() { + $scope.close(); + }, parseInt(dismissOnTimeout, 10)); + } }]) -.directive('alert', function () { +.directive('uibAlert', function() { return { - restrict:'EA', - controller:'AlertController', - templateUrl:'template/alert/alert.html', - transclude:true, - replace:true, + controller: 'UibAlertController', + controllerAs: 'alert', + templateUrl: function(element, attrs) { + return attrs.templateUrl || 'uib/template/alert/alert.html'; + }, + transclude: true, + replace: true, scope: { type: '@', close: '&' @@ -316,62 +273,69 @@ angular.module('ui.bootstrap.alert', []) }; }); -angular.module('ui.bootstrap.bindHtml', []) - - .directive('bindHtmlUnsafe', function () { - return function (scope, element, attr) { - element.addClass('ng-binding').data('$binding', attr.bindHtmlUnsafe); - scope.$watch(attr.bindHtmlUnsafe, function bindHtmlUnsafeWatchAction(value) { - element.html(value || ''); - }); - }; - }); angular.module('ui.bootstrap.buttons', []) -.constant('buttonConfig', { +.constant('uibButtonConfig', { activeClass: 'active', toggleEvent: 'click' }) -.controller('ButtonsController', ['buttonConfig', function(buttonConfig) { +.controller('UibButtonsController', ['uibButtonConfig', function(buttonConfig) { this.activeClass = buttonConfig.activeClass || 'active'; this.toggleEvent = buttonConfig.toggleEvent || 'click'; }]) -.directive('btnRadio', function () { +.directive('uibBtnRadio', ['$parse', function($parse) { return { - require: ['btnRadio', 'ngModel'], - controller: 'ButtonsController', - link: function (scope, element, attrs, ctrls) { + require: ['uibBtnRadio', 'ngModel'], + controller: 'UibButtonsController', + controllerAs: 'buttons', + link: function(scope, element, attrs, ctrls) { var buttonsCtrl = ctrls[0], ngModelCtrl = ctrls[1]; + var uncheckableExpr = $parse(attrs.uibUncheckable); + + element.find('input').css({display: 'none'}); //model -> UI - ngModelCtrl.$render = function () { - element.toggleClass(buttonsCtrl.activeClass, angular.equals(ngModelCtrl.$modelValue, scope.$eval(attrs.btnRadio))); + ngModelCtrl.$render = function() { + element.toggleClass(buttonsCtrl.activeClass, angular.equals(ngModelCtrl.$modelValue, scope.$eval(attrs.uibBtnRadio))); }; //ui->model - element.bind(buttonsCtrl.toggleEvent, function () { + element.on(buttonsCtrl.toggleEvent, function() { + if (attrs.disabled) { + return; + } + var isActive = element.hasClass(buttonsCtrl.activeClass); if (!isActive || angular.isDefined(attrs.uncheckable)) { - scope.$apply(function () { - ngModelCtrl.$setViewValue(isActive ? null : scope.$eval(attrs.btnRadio)); + scope.$apply(function() { + ngModelCtrl.$setViewValue(isActive ? null : scope.$eval(attrs.uibBtnRadio)); ngModelCtrl.$render(); }); } }); + + if (attrs.uibUncheckable) { + scope.$watch(uncheckableExpr, function(uncheckable) { + attrs.$set('uncheckable', uncheckable ? '' : undefined); + }); + } } }; -}) +}]) -.directive('btnCheckbox', function () { +.directive('uibBtnCheckbox', function() { return { - require: ['btnCheckbox', 'ngModel'], - controller: 'ButtonsController', - link: function (scope, element, attrs, ctrls) { + require: ['uibBtnCheckbox', 'ngModel'], + controller: 'UibButtonsController', + controllerAs: 'button', + link: function(scope, element, attrs, ctrls) { var buttonsCtrl = ctrls[0], ngModelCtrl = ctrls[1]; + element.find('input').css({display: 'none'}); + function getTrueValue() { return getCheckboxValue(attrs.btnCheckboxTrue, true); } @@ -380,19 +344,22 @@ angular.module('ui.bootstrap.buttons', []) return getCheckboxValue(attrs.btnCheckboxFalse, false); } - function getCheckboxValue(attributeValue, defaultValue) { - var val = scope.$eval(attributeValue); - return angular.isDefined(val) ? val : defaultValue; + function getCheckboxValue(attribute, defaultValue) { + return angular.isDefined(attribute) ? scope.$eval(attribute) : defaultValue; } //model -> UI - ngModelCtrl.$render = function () { + ngModelCtrl.$render = function() { element.toggleClass(buttonsCtrl.activeClass, angular.equals(ngModelCtrl.$modelValue, getTrueValue())); }; //ui->model - element.bind(buttonsCtrl.toggleEvent, function () { - scope.$apply(function () { + element.on(buttonsCtrl.toggleEvent, function() { + if (attrs.disabled) { + return; + } + + scope.$apply(function() { ngModelCtrl.$setViewValue(element.hasClass(buttonsCtrl.activeClass) ? getFalseValue() : getTrueValue()); ngModelCtrl.$render(); }); @@ -401,141 +368,139 @@ angular.module('ui.bootstrap.buttons', []) }; }); -/** -* @ngdoc overview -* @name ui.bootstrap.carousel -* -* @description -* AngularJS version of an image carousel. -* -*/ -angular.module('ui.bootstrap.carousel', ['ui.bootstrap.transition']) -.controller('CarouselController', ['$scope', '$timeout', '$transition', function ($scope, $timeout, $transition) { +angular.module('ui.bootstrap.carousel', []) + +.controller('UibCarouselController', ['$scope', '$element', '$interval', '$timeout', '$animate', function($scope, $element, $interval, $timeout, $animate) { var self = this, slides = self.slides = $scope.slides = [], - currentIndex = -1, - currentTimeout, isPlaying; - self.currentSlide = null; + SLIDE_DIRECTION = 'uib-slideDirection', + currentIndex = $scope.active, + currentInterval, isPlaying, bufferedTransitions = []; var destroyed = false; + + self.addSlide = function(slide, element) { + slides.push({ + slide: slide, + element: element + }); + slides.sort(function(a, b) { + return +a.slide.index - +b.slide.index; + }); + //if this is the first slide or the slide is set to active, select it + if (slide.index === $scope.active || slides.length === 1 && !angular.isNumber($scope.active)) { + if ($scope.$currentTransition) { + $scope.$currentTransition = null; + } + + currentIndex = slide.index; + $scope.active = slide.index; + setActive(currentIndex); + self.select(slides[findSlideIndex(slide)]); + if (slides.length === 1) { + $scope.play(); + } + } + }; + + self.getCurrentIndex = function() { + for (var i = 0; i < slides.length; i++) { + if (slides[i].slide.index === currentIndex) { + return i; + } + } + }; + + self.next = $scope.next = function() { + var newIndex = (self.getCurrentIndex() + 1) % slides.length; + + if (newIndex === 0 && $scope.noWrap()) { + $scope.pause(); + return; + } + + return self.select(slides[newIndex], 'next'); + }; + + self.prev = $scope.prev = function() { + var newIndex = self.getCurrentIndex() - 1 < 0 ? slides.length - 1 : self.getCurrentIndex() - 1; + + if ($scope.noWrap() && newIndex === slides.length - 1) { + $scope.pause(); + return; + } + + return self.select(slides[newIndex], 'prev'); + }; + + self.removeSlide = function(slide) { + var index = findSlideIndex(slide); + + var bufferedIndex = bufferedTransitions.indexOf(slides[index]); + if (bufferedIndex !== -1) { + bufferedTransitions.splice(bufferedIndex, 1); + } + + //get the index of the slide inside the carousel + slides.splice(index, 1); + if (slides.length > 0 && currentIndex === index) { + if (index >= slides.length) { + currentIndex = slides.length - 1; + $scope.active = currentIndex; + setActive(currentIndex); + self.select(slides[slides.length - 1]); + } else { + currentIndex = index; + $scope.active = currentIndex; + setActive(currentIndex); + self.select(slides[index]); + } + } else if (currentIndex > index) { + currentIndex--; + $scope.active = currentIndex; + } + + //clean the active value when no more slide + if (slides.length === 0) { + currentIndex = null; + $scope.active = null; + clearBufferedTransitions(); + } + }; + /* direction: "prev" or "next" */ self.select = $scope.select = function(nextSlide, direction) { - var nextIndex = slides.indexOf(nextSlide); + var nextIndex = findSlideIndex(nextSlide.slide); //Decide direction if it's not given if (direction === undefined) { - direction = nextIndex > currentIndex ? 'next' : 'prev'; + direction = nextIndex > self.getCurrentIndex() ? 'next' : 'prev'; } - if (nextSlide && nextSlide !== self.currentSlide) { - if ($scope.$currentTransition) { - $scope.$currentTransition.cancel(); - //Timeout so ng-class in template has time to fix classes for finished slide - $timeout(goNext); - } else { - goNext(); - } - } - function goNext() { - // Scope has been destroyed, stop here. - if (destroyed) { return; } - //If we have a slide to transition from and we have a transition type and we're allowed, go - if (self.currentSlide && angular.isString(direction) && !$scope.noTransition && nextSlide.$element) { - //We shouldn't do class manip in here, but it's the same weird thing bootstrap does. need to fix sometime - nextSlide.$element.addClass(direction); - var reflow = nextSlide.$element[0].offsetWidth; //force reflow - - //Set all other slides to stop doing their stuff for the new transition - angular.forEach(slides, function(slide) { - angular.extend(slide, {direction: '', entering: false, leaving: false, active: false}); - }); - angular.extend(nextSlide, {direction: direction, active: true, entering: true}); - angular.extend(self.currentSlide||{}, {direction: direction, leaving: true}); - - $scope.$currentTransition = $transition(nextSlide.$element, {}); - //We have to create new pointers inside a closure since next & current will change - (function(next,current) { - $scope.$currentTransition.then( - function(){ transitionDone(next, current); }, - function(){ transitionDone(next, current); } - ); - }(nextSlide, self.currentSlide)); - } else { - transitionDone(nextSlide, self.currentSlide); - } - self.currentSlide = nextSlide; - currentIndex = nextIndex; - //every time you change slides, reset the timer - restartTimer(); - } - function transitionDone(next, current) { - angular.extend(next, {direction: '', active: true, leaving: false, entering: false}); - angular.extend(current||{}, {direction: '', active: false, leaving: false, entering: false}); - $scope.$currentTransition = null; + //Prevent this user-triggered transition from occurring if there is already one in progress + if (nextSlide.slide.index !== currentIndex && + !$scope.$currentTransition) { + goNext(nextSlide.slide, nextIndex, direction); + } else if (nextSlide && nextSlide.slide.index !== currentIndex && $scope.$currentTransition) { + bufferedTransitions.push(slides[nextIndex]); } }; - $scope.$on('$destroy', function () { - destroyed = true; - }); /* Allow outside people to call indexOf on slides array */ - self.indexOfSlide = function(slide) { - return slides.indexOf(slide); - }; - - $scope.next = function() { - var newIndex = (currentIndex + 1) % slides.length; - - //Prevent this user-triggered transition from occurring if there is already one in progress - if (!$scope.$currentTransition) { - return self.select(slides[newIndex], 'next'); - } - }; - - $scope.prev = function() { - var newIndex = currentIndex - 1 < 0 ? slides.length - 1 : currentIndex - 1; - - //Prevent this user-triggered transition from occurring if there is already one in progress - if (!$scope.$currentTransition) { - return self.select(slides[newIndex], 'prev'); - } + $scope.indexOfSlide = function(slide) { + return +slide.slide.index; }; $scope.isActive = function(slide) { - return self.currentSlide === slide; + return $scope.active === slide.slide.index; }; - $scope.$watch('interval', restartTimer); - $scope.$on('$destroy', resetTimer); - - function restartTimer() { - resetTimer(); - var interval = +$scope.interval; - if (!isNaN(interval) && interval>=0) { - currentTimeout = $timeout(timerFn, interval); - } - } - - function resetTimer() { - if (currentTimeout) { - $timeout.cancel(currentTimeout); - currentTimeout = null; - } - } - - function timerFn() { - if (isPlaying) { - $scope.next(); - restartTimer(); - } else { - $scope.pause(); - } - } - - $scope.play = function() { - if (!isPlaying) { - isPlaying = true; - restartTimer(); - } + $scope.isPrevDisabled = function() { + return $scope.active === 0 && $scope.noWrap(); }; + + $scope.isNextDisabled = function() { + return $scope.active === slides.length - 1 && $scope.noWrap(); + }; + $scope.pause = function() { if (!$scope.noPause) { isPlaying = false; @@ -543,141 +508,175 @@ angular.module('ui.bootstrap.carousel', ['ui.bootstrap.transition']) } }; - self.addSlide = function(slide, element) { - slide.$element = element; - slides.push(slide); - //if this is the first slide or the slide is set to active, select it - if(slides.length === 1 || slide.active) { - self.select(slides[slides.length-1]); - if (slides.length == 1) { - $scope.play(); - } - } else { - slide.active = false; + $scope.play = function() { + if (!isPlaying) { + isPlaying = true; + restartTimer(); } }; - self.removeSlide = function(slide) { - //get the index of the slide inside the carousel - var index = slides.indexOf(slide); - slides.splice(index, 1); - if (slides.length > 0 && slide.active) { - if (index >= slides.length) { - self.select(slides[index-1]); - } else { + $scope.$on('$destroy', function() { + destroyed = true; + resetTimer(); + }); + + $scope.$watch('noTransition', function(noTransition) { + $animate.enabled($element, !noTransition); + }); + + $scope.$watch('interval', restartTimer); + + $scope.$watchCollection('slides', resetTransition); + + $scope.$watch('active', function(index) { + if (angular.isNumber(index) && currentIndex !== index) { + for (var i = 0; i < slides.length; i++) { + if (slides[i].slide.index === index) { + index = i; + break; + } + } + + var slide = slides[index]; + if (slide) { + setActive(index); self.select(slides[index]); + currentIndex = index; } - } else if (currentIndex > index) { - currentIndex--; } - }; + }); + function clearBufferedTransitions() { + while (bufferedTransitions.length) { + bufferedTransitions.shift(); + } + } + + function getSlideByIndex(index) { + for (var i = 0, l = slides.length; i < l; ++i) { + if (slides[i].index === index) { + return slides[i]; + } + } + } + + function setActive(index) { + for (var i = 0; i < slides.length; i++) { + slides[i].slide.active = i === index; + } + } + + function goNext(slide, index, direction) { + if (destroyed) { + return; + } + + angular.extend(slide, {direction: direction}); + angular.extend(slides[currentIndex].slide || {}, {direction: direction}); + if ($animate.enabled($element) && !$scope.$currentTransition && + slides[index].element && self.slides.length > 1) { + slides[index].element.data(SLIDE_DIRECTION, slide.direction); + var currentIdx = self.getCurrentIndex(); + + if (angular.isNumber(currentIdx) && slides[currentIdx].element) { + slides[currentIdx].element.data(SLIDE_DIRECTION, slide.direction); + } + + $scope.$currentTransition = true; + $animate.on('addClass', slides[index].element, function(element, phase) { + if (phase === 'close') { + $scope.$currentTransition = null; + $animate.off('addClass', element); + if (bufferedTransitions.length) { + var nextSlide = bufferedTransitions.pop().slide; + var nextIndex = nextSlide.index; + var nextDirection = nextIndex > self.getCurrentIndex() ? 'next' : 'prev'; + clearBufferedTransitions(); + + goNext(nextSlide, nextIndex, nextDirection); + } + } + }); + } + + $scope.active = slide.index; + currentIndex = slide.index; + setActive(index); + + //every time you change slides, reset the timer + restartTimer(); + } + + function findSlideIndex(slide) { + for (var i = 0; i < slides.length; i++) { + if (slides[i].slide === slide) { + return i; + } + } + } + + function resetTimer() { + if (currentInterval) { + $interval.cancel(currentInterval); + currentInterval = null; + } + } + + function resetTransition(slides) { + if (!slides.length) { + $scope.$currentTransition = null; + clearBufferedTransitions(); + } + } + + function restartTimer() { + resetTimer(); + var interval = +$scope.interval; + if (!isNaN(interval) && interval > 0) { + currentInterval = $interval(timerFn, interval); + } + } + + function timerFn() { + var interval = +$scope.interval; + if (isPlaying && !isNaN(interval) && interval > 0 && slides.length) { + $scope.next(); + } else { + $scope.pause(); + } + } }]) -/** - * @ngdoc directive - * @name ui.bootstrap.carousel.directive:carousel - * @restrict EA - * - * @description - * Carousel is the outer container for a set of image 'slides' to showcase. - * - * @param {number=} interval The time, in milliseconds, that it will take the carousel to go to the next slide. - * @param {boolean=} noTransition Whether to disable transitions on the carousel. - * @param {boolean=} noPause Whether to disable pausing on the carousel (by default, the carousel interval pauses on hover). - * - * @example - - - - - - - - - - - - - - - .carousel-indicators { - top: auto; - bottom: 15px; - } - - - */ -.directive('carousel', [function() { +.directive('uibCarousel', function() { return { - restrict: 'EA', transclude: true, replace: true, - controller: 'CarouselController', - require: 'carousel', - templateUrl: 'template/carousel/carousel.html', + controller: 'UibCarouselController', + controllerAs: 'carousel', + templateUrl: function(element, attrs) { + return attrs.templateUrl || 'uib/template/carousel/carousel.html'; + }, scope: { + active: '=', interval: '=', noTransition: '=', - noPause: '=' + noPause: '=', + noWrap: '&' } }; -}]) +}) -/** - * @ngdoc directive - * @name ui.bootstrap.carousel.directive:slide - * @restrict EA - * - * @description - * Creates a slide inside a {@link ui.bootstrap.carousel.directive:carousel carousel}. Must be placed as a child of a carousel element. - * - * @param {boolean=} active Model binding, whether or not this slide is currently active. - * - * @example - - -
        - - - - - - - Interval, in milliseconds: -
        Enter a negative number to stop the interval. -
        -
        - -function CarouselDemoCtrl($scope) { - $scope.myInterval = 5000; -} - - - .carousel-indicators { - top: auto; - bottom: 15px; - } - -
        -*/ - -.directive('slide', function() { +.directive('uibSlide', function() { return { - require: '^carousel', - restrict: 'EA', + require: '^uibCarousel', transclude: true, replace: true, - templateUrl: 'template/carousel/slide.html', + templateUrl: function(element, attrs) { + return attrs.templateUrl || 'uib/template/carousel/slide.html'; + }, scope: { - active: '=?' + actual: '=?', + index: '=?' }, link: function (scope, element, attrs, carouselCtrl) { carouselCtrl.addSlide(scope, element); @@ -685,85 +684,349 @@ function CarouselDemoCtrl($scope) { scope.$on('$destroy', function() { carouselCtrl.removeSlide(scope); }); - - scope.$watch('active', function(active) { - if (active) { - carouselCtrl.select(scope); - } - }); } }; -}); +}) + +.animation('.item', ['$animateCss', +function($animateCss) { + var SLIDE_DIRECTION = 'uib-slideDirection'; + + function removeClass(element, className, callback) { + element.removeClass(className); + if (callback) { + callback(); + } + } + + return { + beforeAddClass: function(element, className, done) { + if (className === 'active') { + var stopped = false; + var direction = element.data(SLIDE_DIRECTION); + var directionClass = direction === 'next' ? 'left' : 'right'; + var removeClassFn = removeClass.bind(this, element, + directionClass + ' ' + direction, done); + element.addClass(direction); + + $animateCss(element, {addClass: directionClass}) + .start() + .done(removeClassFn); + + return function() { + stopped = true; + }; + } + done(); + }, + beforeRemoveClass: function (element, className, done) { + if (className === 'active') { + var stopped = false; + var direction = element.data(SLIDE_DIRECTION); + var directionClass = direction === 'next' ? 'left' : 'right'; + var removeClassFn = removeClass.bind(this, element, directionClass, done); + + $animateCss(element, {addClass: directionClass}) + .start() + .done(removeClassFn); + + return function() { + stopped = true; + }; + } + done(); + } + }; +}]); angular.module('ui.bootstrap.dateparser', []) -.service('dateParser', ['$locale', 'orderByFilter', function($locale, orderByFilter) { +.service('uibDateParser', ['$log', '$locale', 'dateFilter', 'orderByFilter', function($log, $locale, dateFilter, orderByFilter) { + // Pulled from https://github.com/mbostock/d3/blob/master/src/format/requote.js + var SPECIAL_CHARACTERS_REGEXP = /[\\\^\$\*\+\?\|\[\]\(\)\.\{\}]/g; - this.parsers = {}; + var localeId; + var formatCodeToRegex; - var formatCodeToRegex = { - 'yyyy': { - regex: '\\d{4}', - apply: function(value) { this.year = +value; } - }, - 'yy': { - regex: '\\d{2}', - apply: function(value) { this.year = +value + 2000; } - }, - 'y': { - regex: '\\d{1,4}', - apply: function(value) { this.year = +value; } - }, - 'MMMM': { - regex: $locale.DATETIME_FORMATS.MONTH.join('|'), - apply: function(value) { this.month = $locale.DATETIME_FORMATS.MONTH.indexOf(value); } - }, - 'MMM': { - regex: $locale.DATETIME_FORMATS.SHORTMONTH.join('|'), - apply: function(value) { this.month = $locale.DATETIME_FORMATS.SHORTMONTH.indexOf(value); } - }, - 'MM': { - regex: '0[1-9]|1[0-2]', - apply: function(value) { this.month = value - 1; } - }, - 'M': { - regex: '[1-9]|1[0-2]', - apply: function(value) { this.month = value - 1; } - }, - 'dd': { - regex: '[0-2][0-9]{1}|3[0-1]{1}', - apply: function(value) { this.date = +value; } - }, - 'd': { - regex: '[1-2]?[0-9]{1}|3[0-1]{1}', - apply: function(value) { this.date = +value; } - }, - 'EEEE': { - regex: $locale.DATETIME_FORMATS.DAY.join('|') - }, - 'EEE': { - regex: $locale.DATETIME_FORMATS.SHORTDAY.join('|') - } + this.init = function() { + localeId = $locale.id; + + this.parsers = {}; + this.formatters = {}; + + formatCodeToRegex = [ + { + key: 'yyyy', + regex: '\\d{4}', + apply: function(value) { this.year = +value; }, + formatter: function(date) { + var _date = new Date(); + _date.setFullYear(Math.abs(date.getFullYear())); + return dateFilter(_date, 'yyyy'); + } + }, + { + key: 'yy', + regex: '\\d{2}', + apply: function(value) { value = +value; this.year = value < 69 ? value + 2000 : value + 1900; }, + formatter: function(date) { + var _date = new Date(); + _date.setFullYear(Math.abs(date.getFullYear())); + return dateFilter(_date, 'yy'); + } + }, + { + key: 'y', + regex: '\\d{1,4}', + apply: function(value) { this.year = +value; }, + formatter: function(date) { + var _date = new Date(); + _date.setFullYear(Math.abs(date.getFullYear())); + return dateFilter(_date, 'y'); + } + }, + { + key: 'M!', + regex: '0?[1-9]|1[0-2]', + apply: function(value) { this.month = value - 1; }, + formatter: function(date) { + var value = date.getMonth(); + if (/^[0-9]$/.test(value)) { + return dateFilter(date, 'MM'); + } + + return dateFilter(date, 'M'); + } + }, + { + key: 'MMMM', + regex: $locale.DATETIME_FORMATS.MONTH.join('|'), + apply: function(value) { this.month = $locale.DATETIME_FORMATS.MONTH.indexOf(value); }, + formatter: function(date) { return dateFilter(date, 'MMMM'); } + }, + { + key: 'MMM', + regex: $locale.DATETIME_FORMATS.SHORTMONTH.join('|'), + apply: function(value) { this.month = $locale.DATETIME_FORMATS.SHORTMONTH.indexOf(value); }, + formatter: function(date) { return dateFilter(date, 'MMM'); } + }, + { + key: 'MM', + regex: '0[1-9]|1[0-2]', + apply: function(value) { this.month = value - 1; }, + formatter: function(date) { return dateFilter(date, 'MM'); } + }, + { + key: 'M', + regex: '[1-9]|1[0-2]', + apply: function(value) { this.month = value - 1; }, + formatter: function(date) { return dateFilter(date, 'M'); } + }, + { + key: 'd!', + regex: '[0-2]?[0-9]{1}|3[0-1]{1}', + apply: function(value) { this.date = +value; }, + formatter: function(date) { + var value = date.getDate(); + if (/^[1-9]$/.test(value)) { + return dateFilter(date, 'dd'); + } + + return dateFilter(date, 'd'); + } + }, + { + key: 'dd', + regex: '[0-2][0-9]{1}|3[0-1]{1}', + apply: function(value) { this.date = +value; }, + formatter: function(date) { return dateFilter(date, 'dd'); } + }, + { + key: 'd', + regex: '[1-2]?[0-9]{1}|3[0-1]{1}', + apply: function(value) { this.date = +value; }, + formatter: function(date) { return dateFilter(date, 'd'); } + }, + { + key: 'EEEE', + regex: $locale.DATETIME_FORMATS.DAY.join('|'), + formatter: function(date) { return dateFilter(date, 'EEEE'); } + }, + { + key: 'EEE', + regex: $locale.DATETIME_FORMATS.SHORTDAY.join('|'), + formatter: function(date) { return dateFilter(date, 'EEE'); } + }, + { + key: 'HH', + regex: '(?:0|1)[0-9]|2[0-3]', + apply: function(value) { this.hours = +value; }, + formatter: function(date) { return dateFilter(date, 'HH'); } + }, + { + key: 'hh', + regex: '0[0-9]|1[0-2]', + apply: function(value) { this.hours = +value; }, + formatter: function(date) { return dateFilter(date, 'hh'); } + }, + { + key: 'H', + regex: '1?[0-9]|2[0-3]', + apply: function(value) { this.hours = +value; }, + formatter: function(date) { return dateFilter(date, 'H'); } + }, + { + key: 'h', + regex: '[0-9]|1[0-2]', + apply: function(value) { this.hours = +value; }, + formatter: function(date) { return dateFilter(date, 'h'); } + }, + { + key: 'mm', + regex: '[0-5][0-9]', + apply: function(value) { this.minutes = +value; }, + formatter: function(date) { return dateFilter(date, 'mm'); } + }, + { + key: 'm', + regex: '[0-9]|[1-5][0-9]', + apply: function(value) { this.minutes = +value; }, + formatter: function(date) { return dateFilter(date, 'm'); } + }, + { + key: 'sss', + regex: '[0-9][0-9][0-9]', + apply: function(value) { this.milliseconds = +value; }, + formatter: function(date) { return dateFilter(date, 'sss'); } + }, + { + key: 'ss', + regex: '[0-5][0-9]', + apply: function(value) { this.seconds = +value; }, + formatter: function(date) { return dateFilter(date, 'ss'); } + }, + { + key: 's', + regex: '[0-9]|[1-5][0-9]', + apply: function(value) { this.seconds = +value; }, + formatter: function(date) { return dateFilter(date, 's'); } + }, + { + key: 'a', + regex: $locale.DATETIME_FORMATS.AMPMS.join('|'), + apply: function(value) { + if (this.hours === 12) { + this.hours = 0; + } + + if (value === 'PM') { + this.hours += 12; + } + }, + formatter: function(date) { return dateFilter(date, 'a'); } + }, + { + key: 'Z', + regex: '[+-]\\d{4}', + apply: function(value) { + var matches = value.match(/([+-])(\d{2})(\d{2})/), + sign = matches[1], + hours = matches[2], + minutes = matches[3]; + this.hours += toInt(sign + hours); + this.minutes += toInt(sign + minutes); + }, + formatter: function(date) { + return dateFilter(date, 'Z'); + } + }, + { + key: 'ww', + regex: '[0-4][0-9]|5[0-3]', + formatter: function(date) { return dateFilter(date, 'ww'); } + }, + { + key: 'w', + regex: '[0-9]|[1-4][0-9]|5[0-3]', + formatter: function(date) { return dateFilter(date, 'w'); } + }, + { + key: 'GGGG', + regex: $locale.DATETIME_FORMATS.ERANAMES.join('|').replace(/\s/g, '\\s'), + formatter: function(date) { return dateFilter(date, 'GGGG'); } + }, + { + key: 'GGG', + regex: $locale.DATETIME_FORMATS.ERAS.join('|'), + formatter: function(date) { return dateFilter(date, 'GGG'); } + }, + { + key: 'GG', + regex: $locale.DATETIME_FORMATS.ERAS.join('|'), + formatter: function(date) { return dateFilter(date, 'GG'); } + }, + { + key: 'G', + regex: $locale.DATETIME_FORMATS.ERAS.join('|'), + formatter: function(date) { return dateFilter(date, 'G'); } + } + ]; }; - this.createParser = function(format) { + this.init(); + + function createParser(format, func) { var map = [], regex = format.split(''); - angular.forEach(formatCodeToRegex, function(data, code) { - var index = format.indexOf(code); + // check for literal values + var quoteIndex = format.indexOf('\''); + if (quoteIndex > -1) { + var inLiteral = false; + format = format.split(''); + for (var i = quoteIndex; i < format.length; i++) { + if (inLiteral) { + if (format[i] === '\'') { + if (i + 1 < format.length && format[i+1] === '\'') { // escaped single quote + format[i+1] = '$'; + regex[i+1] = ''; + } else { // end of literal + regex[i] = ''; + inLiteral = false; + } + } + format[i] = '$'; + } else { + if (format[i] === '\'') { // start of literal + format[i] = '$'; + regex[i] = ''; + inLiteral = true; + } + } + } + + format = format.join(''); + } + + angular.forEach(formatCodeToRegex, function(data) { + var index = format.indexOf(data.key); if (index > -1) { format = format.split(''); regex[index] = '(' + data.regex + ')'; format[index] = '$'; // Custom symbol to define consumed part of format - for (var i = index + 1, n = index + code.length; i < n; i++) { + for (var i = index + 1, n = index + data.key.length; i < n; i++) { regex[i] = ''; format[i] = '$'; } format = format.join(''); - map.push({ index: index, apply: data.apply }); + map.push({ + index: index, + key: data.key, + apply: data[func], + matcher: data.regex + }); } }); @@ -771,36 +1034,113 @@ angular.module('ui.bootstrap.dateparser', []) regex: new RegExp('^' + regex.join('') + '$'), map: orderByFilter(map, 'index') }; - }; + } - this.parse = function(input, format) { - if ( !angular.isString(input) ) { - return input; + this.filter = function(date, format) { + if (!angular.isDate(date) || isNaN(date) || !format) { + return ''; } format = $locale.DATETIME_FORMATS[format] || format; - if ( !this.parsers[format] ) { - this.parsers[format] = this.createParser(format); + if ($locale.id !== localeId) { + this.init(); + } + + if (!this.formatters[format]) { + this.formatters[format] = createParser(format, 'formatter'); + } + + var parser = this.formatters[format], + map = parser.map; + + var _format = format; + + return map.reduce(function(str, mapper, i) { + var match = _format.match(new RegExp('(.*)' + mapper.key)); + if (match && angular.isString(match[1])) { + str += match[1]; + _format = _format.replace(match[1] + mapper.key, ''); + } + + var endStr = i === map.length - 1 ? _format : ''; + + if (mapper.apply) { + return str + mapper.apply.call(null, date) + endStr; + } + + return str + endStr; + }, ''); + }; + + this.parse = function(input, format, baseDate) { + if (!angular.isString(input) || !format) { + return input; + } + + format = $locale.DATETIME_FORMATS[format] || format; + format = format.replace(SPECIAL_CHARACTERS_REGEXP, '\\$&'); + + if ($locale.id !== localeId) { + this.init(); + } + + if (!this.parsers[format]) { + this.parsers[format] = createParser(format, 'apply'); } var parser = this.parsers[format], regex = parser.regex, map = parser.map, - results = input.match(regex); + results = input.match(regex), + tzOffset = false; + if (results && results.length) { + var fields, dt; + if (angular.isDate(baseDate) && !isNaN(baseDate.getTime())) { + fields = { + year: baseDate.getFullYear(), + month: baseDate.getMonth(), + date: baseDate.getDate(), + hours: baseDate.getHours(), + minutes: baseDate.getMinutes(), + seconds: baseDate.getSeconds(), + milliseconds: baseDate.getMilliseconds() + }; + } else { + if (baseDate) { + $log.warn('dateparser:', 'baseDate is not a valid date'); + } + fields = { year: 1900, month: 0, date: 1, hours: 0, minutes: 0, seconds: 0, milliseconds: 0 }; + } - if ( results && results.length ) { - var fields = { year: 1900, month: 0, date: 1, hours: 0 }, dt; + for (var i = 1, n = results.length; i < n; i++) { + var mapper = map[i - 1]; + if (mapper.matcher === 'Z') { + tzOffset = true; + } - for( var i = 1, n = results.length; i < n; i++ ) { - var mapper = map[i-1]; - if ( mapper.apply ) { + if (mapper.apply) { mapper.apply.call(fields, results[i]); } } - if ( isValid(fields.year, fields.month, fields.date) ) { - dt = new Date( fields.year, fields.month, fields.date, fields.hours); + var datesetter = tzOffset ? Date.prototype.setUTCFullYear : + Date.prototype.setFullYear; + var timesetter = tzOffset ? Date.prototype.setUTCHours : + Date.prototype.setHours; + + if (isValid(fields.year, fields.month, fields.date)) { + if (angular.isDate(baseDate) && !isNaN(baseDate.getTime()) && !tzOffset) { + dt = new Date(baseDate); + datesetter.call(dt, fields.year, fields.month, fields.date); + timesetter.call(dt, fields.hours, fields.minutes, + fields.seconds, fields.milliseconds); + } else { + dt = new Date(0); + datesetter.call(dt, fields.year, fields.month, fields.date); + timesetter.call(dt, fields.hours || 0, fields.minutes || 0, + fields.seconds || 0, fields.milliseconds || 0); + } } return dt; @@ -810,218 +1150,302 @@ angular.module('ui.bootstrap.dateparser', []) // Check if date is valid for specific month (and year for February). // Month: 0 = Jan, 1 = Feb, etc function isValid(year, month, date) { - if ( month === 1 && date > 28) { - return date === 29 && ((year % 4 === 0 && year % 100 !== 0) || year % 400 === 0); + if (date < 1) { + return false; } - if ( month === 3 || month === 5 || month === 8 || month === 10) { - return date < 31; + if (month === 1 && date > 28) { + return date === 29 && (year % 4 === 0 && year % 100 !== 0 || year % 400 === 0); + } + + if (month === 3 || month === 5 || month === 8 || month === 10) { + return date < 31; } return true; } + + function toInt(str) { + return parseInt(str, 10); + } + + this.toTimezone = toTimezone; + this.fromTimezone = fromTimezone; + this.timezoneToOffset = timezoneToOffset; + this.addDateMinutes = addDateMinutes; + this.convertTimezoneToLocal = convertTimezoneToLocal; + + function toTimezone(date, timezone) { + return date && timezone ? convertTimezoneToLocal(date, timezone) : date; + } + + function fromTimezone(date, timezone) { + return date && timezone ? convertTimezoneToLocal(date, timezone, true) : date; + } + + //https://github.com/angular/angular.js/blob/4daafd3dbe6a80d578f5a31df1bb99c77559543e/src/Angular.js#L1207 + function timezoneToOffset(timezone, fallback) { + var requestedTimezoneOffset = Date.parse('Jan 01, 1970 00:00:00 ' + timezone) / 60000; + return isNaN(requestedTimezoneOffset) ? fallback : requestedTimezoneOffset; + } + + function addDateMinutes(date, minutes) { + date = new Date(date.getTime()); + date.setMinutes(date.getMinutes() + minutes); + return date; + } + + function convertTimezoneToLocal(date, timezone, reverse) { + reverse = reverse ? -1 : 1; + var timezoneOffset = timezoneToOffset(timezone, date.getTimezoneOffset()); + return addDateMinutes(date, reverse * (timezoneOffset - date.getTimezoneOffset())); + } }]); -angular.module('ui.bootstrap.position', []) +// Avoiding use of ng-class as it creates a lot of watchers when a class is to be applied to +// at most one element. +angular.module('ui.bootstrap.isClass', []) +.directive('uibIsClass', [ + '$animate', +function ($animate) { + // 11111111 22222222 + var ON_REGEXP = /^\s*([\s\S]+?)\s+on\s+([\s\S]+?)\s*$/; + // 11111111 22222222 + var IS_REGEXP = /^\s*([\s\S]+?)\s+for\s+([\s\S]+?)\s*$/; -/** - * A set of utility methods that can be use to retrieve position of DOM elements. - * It is meant to be used where we need to absolute-position DOM elements in - * relation to other, existing elements (this is the case for tooltips, popovers, - * typeahead suggestions etc.). - */ - .factory('$position', ['$document', '$window', function ($document, $window) { + var dataPerTracked = {}; - function getStyle(el, cssprop) { - if (el.currentStyle) { //IE - return el.currentStyle[cssprop]; - } else if ($window.getComputedStyle) { - return $window.getComputedStyle(el)[cssprop]; + return { + restrict: 'A', + compile: function(tElement, tAttrs) { + var linkedScopes = []; + var instances = []; + var expToData = {}; + var lastActivated = null; + var onExpMatches = tAttrs.uibIsClass.match(ON_REGEXP); + var onExp = onExpMatches[2]; + var expsStr = onExpMatches[1]; + var exps = expsStr.split(','); + + return linkFn; + + function linkFn(scope, element, attrs) { + linkedScopes.push(scope); + instances.push({ + scope: scope, + element: element + }); + + exps.forEach(function(exp, k) { + addForExp(exp, scope); + }); + + scope.$on('$destroy', removeScope); } - // finally try and get inline style - return el.style[cssprop]; - } - /** - * Checks if a given element is statically positioned - * @param element - raw DOM element - */ - function isStaticPositioned(element) { - return (getStyle(element, 'position') || 'static' ) === 'static'; - } - - /** - * returns the closest, non-statically positioned parentOffset of a given element - * @param element - */ - var parentOffsetEl = function (element) { - var docDomEl = $document[0]; - var offsetParent = element.offsetParent || docDomEl; - while (offsetParent && offsetParent !== docDomEl && isStaticPositioned(offsetParent) ) { - offsetParent = offsetParent.offsetParent; - } - return offsetParent || docDomEl; - }; - - return { - /** - * Provides read-only equivalent of jQuery's position function: - * http://api.jquery.com/position/ - */ - position: function (element) { - var elBCR = this.offset(element); - var offsetParentBCR = { top: 0, left: 0 }; - var offsetParentEl = parentOffsetEl(element[0]); - if (offsetParentEl != $document[0]) { - offsetParentBCR = this.offset(angular.element(offsetParentEl)); - offsetParentBCR.top += offsetParentEl.clientTop - offsetParentEl.scrollTop; - offsetParentBCR.left += offsetParentEl.clientLeft - offsetParentEl.scrollLeft; + function addForExp(exp, scope) { + var matches = exp.match(IS_REGEXP); + var clazz = scope.$eval(matches[1]); + var compareWithExp = matches[2]; + var data = expToData[exp]; + if (!data) { + var watchFn = function(compareWithVal) { + var newActivated = null; + instances.some(function(instance) { + var thisVal = instance.scope.$eval(onExp); + if (thisVal === compareWithVal) { + newActivated = instance; + return true; + } + }); + if (data.lastActivated !== newActivated) { + if (data.lastActivated) { + $animate.removeClass(data.lastActivated.element, clazz); + } + if (newActivated) { + $animate.addClass(newActivated.element, clazz); + } + data.lastActivated = newActivated; + } + }; + expToData[exp] = data = { + lastActivated: null, + scope: scope, + watchFn: watchFn, + compareWithExp: compareWithExp, + watcher: scope.$watch(compareWithExp, watchFn) + }; } - - var boundingClientRect = element[0].getBoundingClientRect(); - return { - width: boundingClientRect.width || element.prop('offsetWidth'), - height: boundingClientRect.height || element.prop('offsetHeight'), - top: elBCR.top - offsetParentBCR.top, - left: elBCR.left - offsetParentBCR.left - }; - }, - - /** - * Provides read-only equivalent of jQuery's offset function: - * http://api.jquery.com/offset/ - */ - offset: function (element) { - var boundingClientRect = element[0].getBoundingClientRect(); - return { - width: boundingClientRect.width || element.prop('offsetWidth'), - height: boundingClientRect.height || element.prop('offsetHeight'), - top: boundingClientRect.top + ($window.pageYOffset || $document[0].documentElement.scrollTop), - left: boundingClientRect.left + ($window.pageXOffset || $document[0].documentElement.scrollLeft) - }; - }, - - /** - * Provides coordinates for the targetEl in relation to hostEl - */ - positionElements: function (hostEl, targetEl, positionStr, appendToBody) { - - var positionStrParts = positionStr.split('-'); - var pos0 = positionStrParts[0], pos1 = positionStrParts[1] || 'center'; - - var hostElPos, - targetElWidth, - targetElHeight, - targetElPos; - - hostElPos = appendToBody ? this.offset(hostEl) : this.position(hostEl); - - targetElWidth = targetEl.prop('offsetWidth'); - targetElHeight = targetEl.prop('offsetHeight'); - - var shiftWidth = { - center: function () { - return hostElPos.left + hostElPos.width / 2 - targetElWidth / 2; - }, - left: function () { - return hostElPos.left; - }, - right: function () { - return hostElPos.left + hostElPos.width; - } - }; - - var shiftHeight = { - center: function () { - return hostElPos.top + hostElPos.height / 2 - targetElHeight / 2; - }, - top: function () { - return hostElPos.top; - }, - bottom: function () { - return hostElPos.top + hostElPos.height; - } - }; - - switch (pos0) { - case 'right': - targetElPos = { - top: shiftHeight[pos1](), - left: shiftWidth[pos0]() - }; - break; - case 'left': - targetElPos = { - top: shiftHeight[pos1](), - left: hostElPos.left - targetElWidth - }; - break; - case 'bottom': - targetElPos = { - top: shiftHeight[pos0](), - left: shiftWidth[pos1]() - }; - break; - default: - targetElPos = { - top: hostElPos.top - targetElHeight, - left: shiftWidth[pos1]() - }; - break; - } - - return targetElPos; + data.watchFn(scope.$eval(compareWithExp)); } - }; - }]); -angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootstrap.position']) + function removeScope(e) { + var removedScope = e.targetScope; + var index = linkedScopes.indexOf(removedScope); + linkedScopes.splice(index, 1); + instances.splice(index, 1); + if (linkedScopes.length) { + var newWatchScope = linkedScopes[0]; + angular.forEach(expToData, function(data) { + if (data.scope === removedScope) { + data.watcher = newWatchScope.$watch(data.compareWithExp, data.watchFn); + data.scope = newWatchScope; + } + }); + } else { + expToData = {}; + } + } + } + }; +}]); +angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootstrap.isClass']) -.constant('datepickerConfig', { +.value('$datepickerSuppressError', false) + +.value('$datepickerLiteralWarning', true) + +.constant('uibDatepickerConfig', { + datepickerMode: 'day', formatDay: 'dd', formatMonth: 'MMMM', formatYear: 'yyyy', formatDayHeader: 'EEE', formatDayTitle: 'MMMM yyyy', formatMonthTitle: 'yyyy', - datepickerMode: 'day', - minMode: 'day', + maxDate: null, maxMode: 'year', - showWeeks: true, - startingDay: 0, - yearRange: 20, minDate: null, - maxDate: null + minMode: 'day', + ngModelOptions: {}, + shortcutPropagation: false, + showWeeks: true, + yearColumns: 5, + yearRows: 4 }) -.controller('DatepickerController', ['$scope', '$attrs', '$parse', '$interpolate', '$timeout', '$log', 'dateFilter', 'datepickerConfig', function($scope, $attrs, $parse, $interpolate, $timeout, $log, dateFilter, datepickerConfig) { +.controller('UibDatepickerController', ['$scope', '$attrs', '$parse', '$interpolate', '$locale', '$log', 'dateFilter', 'uibDatepickerConfig', '$datepickerLiteralWarning', '$datepickerSuppressError', 'uibDateParser', + function($scope, $attrs, $parse, $interpolate, $locale, $log, dateFilter, datepickerConfig, $datepickerLiteralWarning, $datepickerSuppressError, dateParser) { var self = this, - ngModelCtrl = { $setViewValue: angular.noop }; // nullModelCtrl; + ngModelCtrl = { $setViewValue: angular.noop }, // nullModelCtrl; + ngModelOptions = {}, + watchListeners = [], + optionsUsed = !!$attrs.datepickerOptions; + + if (!$scope.datepickerOptions) { + $scope.datepickerOptions = {}; + } // Modes chain this.modes = ['day', 'month', 'year']; - // Configuration attributes - angular.forEach(['formatDay', 'formatMonth', 'formatYear', 'formatDayHeader', 'formatDayTitle', 'formatMonthTitle', - 'minMode', 'maxMode', 'showWeeks', 'startingDay', 'yearRange'], function( key, index ) { - self[key] = angular.isDefined($attrs[key]) ? (index < 8 ? $interpolate($attrs[key])($scope.$parent) : $scope.$parent.$eval($attrs[key])) : datepickerConfig[key]; - }); + [ + 'customClass', + 'dateDisabled', + 'datepickerMode', + 'formatDay', + 'formatDayHeader', + 'formatDayTitle', + 'formatMonth', + 'formatMonthTitle', + 'formatYear', + 'maxDate', + 'maxMode', + 'minDate', + 'minMode', + 'showWeeks', + 'shortcutPropagation', + 'startingDay', + 'yearColumns', + 'yearRows' + ].forEach(function(key) { + switch (key) { + case 'customClass': + case 'dateDisabled': + $scope[key] = $scope.datepickerOptions[key] || angular.noop; + break; + case 'datepickerMode': + $scope.datepickerMode = angular.isDefined($scope.datepickerOptions.datepickerMode) ? + $scope.datepickerOptions.datepickerMode : datepickerConfig.datepickerMode; + break; + case 'formatDay': + case 'formatDayHeader': + case 'formatDayTitle': + case 'formatMonth': + case 'formatMonthTitle': + case 'formatYear': + self[key] = angular.isDefined($scope.datepickerOptions[key]) ? + $interpolate($scope.datepickerOptions[key])($scope.$parent) : + datepickerConfig[key]; + break; + case 'showWeeks': + case 'shortcutPropagation': + case 'yearColumns': + case 'yearRows': + self[key] = angular.isDefined($scope.datepickerOptions[key]) ? + $scope.datepickerOptions[key] : datepickerConfig[key]; + break; + case 'startingDay': + if (angular.isDefined($scope.datepickerOptions.startingDay)) { + self.startingDay = $scope.datepickerOptions.startingDay; + } else if (angular.isNumber(datepickerConfig.startingDay)) { + self.startingDay = datepickerConfig.startingDay; + } else { + self.startingDay = ($locale.DATETIME_FORMATS.FIRSTDAYOFWEEK + 8) % 7; + } - // Watchable attributes - angular.forEach(['minDate', 'maxDate'], function( key ) { - if ( $attrs[key] ) { - $scope.$parent.$watch($parse($attrs[key]), function(value) { - self[key] = value ? new Date(value) : null; - self.refreshView(); - }); - } else { - self[key] = datepickerConfig[key] ? new Date(datepickerConfig[key]) : null; + break; + case 'maxDate': + case 'minDate': + $scope.$watch('datepickerOptions.' + key, function(value) { + if (value) { + if (angular.isDate(value)) { + self[key] = dateParser.fromTimezone(new Date(value), ngModelOptions.timezone); + } else { + if ($datepickerLiteralWarning) { + $log.warn('Literal date support has been deprecated, please switch to date object usage'); + } + + self[key] = new Date(dateFilter(value, 'medium')); + } + } else { + self[key] = datepickerConfig[key] ? + dateParser.fromTimezone(new Date(datepickerConfig[key]), ngModelOptions.timezone) : + null; + } + + self.refreshView(); + }); + + break; + case 'maxMode': + case 'minMode': + if ($scope.datepickerOptions[key]) { + $scope.$watch(function() { return $scope.datepickerOptions[key]; }, function(value) { + self[key] = $scope[key] = angular.isDefined(value) ? value : datepickerOptions[key]; + if (key === 'minMode' && self.modes.indexOf($scope.datepickerOptions.datepickerMode) < self.modes.indexOf(self[key]) || + key === 'maxMode' && self.modes.indexOf($scope.datepickerOptions.datepickerMode) > self.modes.indexOf(self[key])) { + $scope.datepickerMode = self[key]; + $scope.datepickerOptions.datepickerMode = self[key]; + } + }); + } else { + self[key] = $scope[key] = datepickerConfig[key] || null; + } + + break; } }); - $scope.datepickerMode = $scope.datepickerMode || datepickerConfig.datepickerMode; $scope.uniqueId = 'datepicker-' + $scope.$id + '-' + Math.floor(Math.random() * 10000); - this.activeDate = angular.isDefined($attrs.initDate) ? $scope.$parent.$eval($attrs.initDate) : new Date(); + + $scope.disabled = angular.isDefined($attrs.disabled) || false; + if (angular.isDefined($attrs.ngDisabled)) { + watchListeners.push($scope.$parent.$watch($attrs.ngDisabled, function(disabled) { + $scope.disabled = disabled; + self.refreshView(); + })); + } $scope.isActive = function(dateObject) { if (self.compare(dateObject.date, self.activeDate) === 0) { @@ -1031,8 +1455,24 @@ angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootst return false; }; - this.init = function( ngModelCtrl_ ) { + this.init = function(ngModelCtrl_) { ngModelCtrl = ngModelCtrl_; + ngModelOptions = ngModelCtrl_.$options || datepickerConfig.ngModelOptions; + if ($scope.datepickerOptions.initDate) { + self.activeDate = dateParser.fromTimezone($scope.datepickerOptions.initDate, ngModelOptions.timezone) || new Date(); + $scope.$watch('datepickerOptions.initDate', function(initDate) { + if (initDate && (ngModelCtrl.$isEmpty(ngModelCtrl.$modelValue) || ngModelCtrl.$invalid)) { + self.activeDate = dateParser.fromTimezone(initDate, ngModelOptions.timezone); + self.refreshView(); + } + }); + } else { + self.activeDate = new Date(); + } + + this.activeDate = ngModelCtrl.$modelValue ? + dateParser.fromTimezone(new Date(ngModelCtrl.$modelValue), ngModelOptions.timezone) : + dateParser.fromTimezone(new Date(), ngModelOptions.timezone); ngModelCtrl.$render = function() { self.render(); @@ -1040,42 +1480,71 @@ angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootst }; this.render = function() { - if ( ngModelCtrl.$modelValue ) { - var date = new Date( ngModelCtrl.$modelValue ), + if (ngModelCtrl.$viewValue) { + var date = new Date(ngModelCtrl.$viewValue), isValid = !isNaN(date); - if ( isValid ) { - this.activeDate = date; - } else { - $log.error('Datepicker directive: "ng-model" value must be a Date object, a number of milliseconds since 01.01.1970 or a string representing an RFC2822 or ISO 8601 date.'); + if (isValid) { + this.activeDate = dateParser.fromTimezone(date, ngModelOptions.timezone); + } else if (!$datepickerSuppressError) { + $log.error('Datepicker directive: "ng-model" value must be a Date object'); } - ngModelCtrl.$setValidity('date', isValid); } this.refreshView(); }; this.refreshView = function() { - if ( this.element ) { + if (this.element) { + $scope.selectedDt = null; this._refreshView(); + if ($scope.activeDt) { + $scope.activeDateId = $scope.activeDt.uid; + } - var date = ngModelCtrl.$modelValue ? new Date(ngModelCtrl.$modelValue) : null; - ngModelCtrl.$setValidity('date-disabled', !date || (this.element && !this.isDisabled(date))); + var date = ngModelCtrl.$viewValue ? new Date(ngModelCtrl.$viewValue) : null; + date = dateParser.fromTimezone(date, ngModelOptions.timezone); + ngModelCtrl.$setValidity('dateDisabled', !date || + this.element && !this.isDisabled(date)); } }; this.createDateObject = function(date, format) { - var model = ngModelCtrl.$modelValue ? new Date(ngModelCtrl.$modelValue) : null; - return { + var model = ngModelCtrl.$viewValue ? new Date(ngModelCtrl.$viewValue) : null; + model = dateParser.fromTimezone(model, ngModelOptions.timezone); + var today = new Date(); + today = dateParser.fromTimezone(today, ngModelOptions.timezone); + var time = this.compare(date, today); + var dt = { date: date, - label: dateFilter(date, format), + label: dateParser.filter(date, format), selected: model && this.compare(date, model) === 0, disabled: this.isDisabled(date), - current: this.compare(date, new Date()) === 0 + past: time < 0, + current: time === 0, + future: time > 0, + customClass: this.customClass(date) || null }; + + if (model && this.compare(date, model) === 0) { + $scope.selectedDt = dt; + } + + if (self.activeDate && this.compare(dt.date, self.activeDate) === 0) { + $scope.activeDt = dt; + } + + return dt; }; - this.isDisabled = function( date ) { - return ((this.minDate && this.compare(date, this.minDate) < 0) || (this.maxDate && this.compare(date, this.maxDate) > 0) || ($attrs.dateDisabled && $scope.dateDisabled({date: date, mode: $scope.datepickerMode}))); + this.isDisabled = function(date) { + return $scope.disabled || + this.minDate && this.compare(date, this.minDate) < 0 || + this.maxDate && this.compare(date, this.maxDate) > 0 || + $scope.dateDisabled && $scope.dateDisabled({date: date, mode: $scope.datepickerMode}); + }; + + this.customClass = function(date) { + return $scope.customClass({date: date, mode: $scope.datepickerMode}); }; // Split array into smaller arrays @@ -1087,579 +1556,1563 @@ angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootst return arrays; }; - $scope.select = function( date ) { - if ( $scope.datepickerMode === self.minMode ) { - var dt = ngModelCtrl.$modelValue ? new Date( ngModelCtrl.$modelValue ) : new Date(0, 0, 0, 0, 0, 0, 0); - dt.setFullYear( date.getFullYear(), date.getMonth(), date.getDate() ); - ngModelCtrl.$setViewValue( dt ); + $scope.select = function(date) { + if ($scope.datepickerMode === self.minMode) { + var dt = ngModelCtrl.$viewValue ? dateParser.fromTimezone(new Date(ngModelCtrl.$viewValue), ngModelOptions.timezone) : new Date(0, 0, 0, 0, 0, 0, 0); + dt.setFullYear(date.getFullYear(), date.getMonth(), date.getDate()); + dt = dateParser.toTimezone(dt, ngModelOptions.timezone); + ngModelCtrl.$setViewValue(dt); ngModelCtrl.$render(); } else { self.activeDate = date; - $scope.datepickerMode = self.modes[ self.modes.indexOf( $scope.datepickerMode ) - 1 ]; + setMode(self.modes[self.modes.indexOf($scope.datepickerMode) - 1]); + + $scope.$emit('uib:datepicker.mode'); } + + $scope.$broadcast('uib:datepicker.focus'); }; - $scope.move = function( direction ) { + $scope.move = function(direction) { var year = self.activeDate.getFullYear() + direction * (self.step.years || 0), month = self.activeDate.getMonth() + direction * (self.step.months || 0); self.activeDate.setFullYear(year, month, 1); self.refreshView(); }; - $scope.toggleMode = function( direction ) { + $scope.toggleMode = function(direction) { direction = direction || 1; - if (($scope.datepickerMode === self.maxMode && direction === 1) || ($scope.datepickerMode === self.minMode && direction === -1)) { + if ($scope.datepickerMode === self.maxMode && direction === 1 || + $scope.datepickerMode === self.minMode && direction === -1) { return; } - $scope.datepickerMode = self.modes[ self.modes.indexOf( $scope.datepickerMode ) + direction ]; + setMode(self.modes[self.modes.indexOf($scope.datepickerMode) + direction]); + + $scope.$emit('uib:datepicker.mode'); }; // Key event mapper - $scope.keys = { 13:'enter', 32:'space', 33:'pageup', 34:'pagedown', 35:'end', 36:'home', 37:'left', 38:'up', 39:'right', 40:'down' }; + $scope.keys = { 13: 'enter', 32: 'space', 33: 'pageup', 34: 'pagedown', 35: 'end', 36: 'home', 37: 'left', 38: 'up', 39: 'right', 40: 'down' }; var focusElement = function() { - $timeout(function() { - self.element[0].focus(); - }, 0 , false); + self.element[0].focus(); }; // Listen for focus requests from popup directive - $scope.$on('datepicker.focus', focusElement); + $scope.$on('uib:datepicker.focus', focusElement); - $scope.keydown = function( evt ) { + $scope.keydown = function(evt) { var key = $scope.keys[evt.which]; - if ( !key || evt.shiftKey || evt.altKey ) { + if (!key || evt.shiftKey || evt.altKey || $scope.disabled) { return; } evt.preventDefault(); - evt.stopPropagation(); + if (!self.shortcutPropagation) { + evt.stopPropagation(); + } if (key === 'enter' || key === 'space') { - if ( self.isDisabled(self.activeDate)) { + if (self.isDisabled(self.activeDate)) { return; // do nothing } $scope.select(self.activeDate); - focusElement(); } else if (evt.ctrlKey && (key === 'up' || key === 'down')) { $scope.toggleMode(key === 'up' ? 1 : -1); - focusElement(); } else { self.handleKeyDown(key, evt); self.refreshView(); } }; + + $scope.$on('$destroy', function() { + //Clear all watch listeners on destroy + while (watchListeners.length) { + watchListeners.shift()(); + } + }); + + function setMode(mode) { + $scope.datepickerMode = mode; + $scope.datepickerOptions.datepickerMode = mode; + } }]) -.directive( 'datepicker', function () { +.controller('UibDaypickerController', ['$scope', '$element', 'dateFilter', function(scope, $element, dateFilter) { + var DAYS_IN_MONTH = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; + + this.step = { months: 1 }; + this.element = $element; + function getDaysInMonth(year, month) { + return month === 1 && year % 4 === 0 && + (year % 100 !== 0 || year % 400 === 0) ? 29 : DAYS_IN_MONTH[month]; + } + + this.init = function(ctrl) { + angular.extend(ctrl, this); + scope.showWeeks = ctrl.showWeeks; + ctrl.refreshView(); + }; + + this.getDates = function(startDate, n) { + var dates = new Array(n), current = new Date(startDate), i = 0, date; + while (i < n) { + date = new Date(current); + dates[i++] = date; + current.setDate(current.getDate() + 1); + } + return dates; + }; + + this._refreshView = function() { + var year = this.activeDate.getFullYear(), + month = this.activeDate.getMonth(), + firstDayOfMonth = new Date(this.activeDate); + + firstDayOfMonth.setFullYear(year, month, 1); + + var difference = this.startingDay - firstDayOfMonth.getDay(), + numDisplayedFromPreviousMonth = difference > 0 ? + 7 - difference : - difference, + firstDate = new Date(firstDayOfMonth); + + if (numDisplayedFromPreviousMonth > 0) { + firstDate.setDate(-numDisplayedFromPreviousMonth + 1); + } + + // 42 is the number of days on a six-week calendar + var days = this.getDates(firstDate, 42); + for (var i = 0; i < 42; i ++) { + days[i] = angular.extend(this.createDateObject(days[i], this.formatDay), { + secondary: days[i].getMonth() !== month, + uid: scope.uniqueId + '-' + i + }); + } + + scope.labels = new Array(7); + for (var j = 0; j < 7; j++) { + scope.labels[j] = { + abbr: dateFilter(days[j].date, this.formatDayHeader), + full: dateFilter(days[j].date, 'EEEE') + }; + } + + scope.title = dateFilter(this.activeDate, this.formatDayTitle); + scope.rows = this.split(days, 7); + + if (scope.showWeeks) { + scope.weekNumbers = []; + var thursdayIndex = (4 + 7 - this.startingDay) % 7, + numWeeks = scope.rows.length; + for (var curWeek = 0; curWeek < numWeeks; curWeek++) { + scope.weekNumbers.push( + getISO8601WeekNumber(scope.rows[curWeek][thursdayIndex].date)); + } + } + }; + + this.compare = function(date1, date2) { + var _date1 = new Date(date1.getFullYear(), date1.getMonth(), date1.getDate()); + var _date2 = new Date(date2.getFullYear(), date2.getMonth(), date2.getDate()); + _date1.setFullYear(date1.getFullYear()); + _date2.setFullYear(date2.getFullYear()); + return _date1 - _date2; + }; + + function getISO8601WeekNumber(date) { + var checkDate = new Date(date); + checkDate.setDate(checkDate.getDate() + 4 - (checkDate.getDay() || 7)); // Thursday + var time = checkDate.getTime(); + checkDate.setMonth(0); // Compare with Jan 1 + checkDate.setDate(1); + return Math.floor(Math.round((time - checkDate) / 86400000) / 7) + 1; + } + + this.handleKeyDown = function(key, evt) { + var date = this.activeDate.getDate(); + + if (key === 'left') { + date = date - 1; + } else if (key === 'up') { + date = date - 7; + } else if (key === 'right') { + date = date + 1; + } else if (key === 'down') { + date = date + 7; + } else if (key === 'pageup' || key === 'pagedown') { + var month = this.activeDate.getMonth() + (key === 'pageup' ? - 1 : 1); + this.activeDate.setMonth(month, 1); + date = Math.min(getDaysInMonth(this.activeDate.getFullYear(), this.activeDate.getMonth()), date); + } else if (key === 'home') { + date = 1; + } else if (key === 'end') { + date = getDaysInMonth(this.activeDate.getFullYear(), this.activeDate.getMonth()); + } + this.activeDate.setDate(date); + }; +}]) + +.controller('UibMonthpickerController', ['$scope', '$element', 'dateFilter', function(scope, $element, dateFilter) { + this.step = { years: 1 }; + this.element = $element; + + this.init = function(ctrl) { + angular.extend(ctrl, this); + ctrl.refreshView(); + }; + + this._refreshView = function() { + var months = new Array(12), + year = this.activeDate.getFullYear(), + date; + + for (var i = 0; i < 12; i++) { + date = new Date(this.activeDate); + date.setFullYear(year, i, 1); + months[i] = angular.extend(this.createDateObject(date, this.formatMonth), { + uid: scope.uniqueId + '-' + i + }); + } + + scope.title = dateFilter(this.activeDate, this.formatMonthTitle); + scope.rows = this.split(months, 3); + }; + + this.compare = function(date1, date2) { + var _date1 = new Date(date1.getFullYear(), date1.getMonth()); + var _date2 = new Date(date2.getFullYear(), date2.getMonth()); + _date1.setFullYear(date1.getFullYear()); + _date2.setFullYear(date2.getFullYear()); + return _date1 - _date2; + }; + + this.handleKeyDown = function(key, evt) { + var date = this.activeDate.getMonth(); + + if (key === 'left') { + date = date - 1; + } else if (key === 'up') { + date = date - 3; + } else if (key === 'right') { + date = date + 1; + } else if (key === 'down') { + date = date + 3; + } else if (key === 'pageup' || key === 'pagedown') { + var year = this.activeDate.getFullYear() + (key === 'pageup' ? - 1 : 1); + this.activeDate.setFullYear(year); + } else if (key === 'home') { + date = 0; + } else if (key === 'end') { + date = 11; + } + this.activeDate.setMonth(date); + }; +}]) + +.controller('UibYearpickerController', ['$scope', '$element', 'dateFilter', function(scope, $element, dateFilter) { + var columns, range; + this.element = $element; + + function getStartingYear(year) { + return parseInt((year - 1) / range, 10) * range + 1; + } + + this.yearpickerInit = function() { + columns = this.yearColumns; + range = this.yearRows * columns; + this.step = { years: range }; + }; + + this._refreshView = function() { + var years = new Array(range), date; + + for (var i = 0, start = getStartingYear(this.activeDate.getFullYear()); i < range; i++) { + date = new Date(this.activeDate); + date.setFullYear(start + i, 0, 1); + years[i] = angular.extend(this.createDateObject(date, this.formatYear), { + uid: scope.uniqueId + '-' + i + }); + } + + scope.title = [years[0].label, years[range - 1].label].join(' - '); + scope.rows = this.split(years, columns); + scope.columns = columns; + }; + + this.compare = function(date1, date2) { + return date1.getFullYear() - date2.getFullYear(); + }; + + this.handleKeyDown = function(key, evt) { + var date = this.activeDate.getFullYear(); + + if (key === 'left') { + date = date - 1; + } else if (key === 'up') { + date = date - columns; + } else if (key === 'right') { + date = date + 1; + } else if (key === 'down') { + date = date + columns; + } else if (key === 'pageup' || key === 'pagedown') { + date += (key === 'pageup' ? - 1 : 1) * range; + } else if (key === 'home') { + date = getStartingYear(this.activeDate.getFullYear()); + } else if (key === 'end') { + date = getStartingYear(this.activeDate.getFullYear()) + range - 1; + } + this.activeDate.setFullYear(date); + }; +}]) + +.directive('uibDatepicker', function() { return { - restrict: 'EA', replace: true, - templateUrl: 'template/datepicker/datepicker.html', - scope: { - datepickerMode: '=?', - dateDisabled: '&' + templateUrl: function(element, attrs) { + return attrs.templateUrl || 'uib/template/datepicker/datepicker.html'; }, - require: ['datepicker', '?^ngModel'], - controller: 'DatepickerController', + scope: { + datepickerOptions: '=?' + }, + require: ['uibDatepicker', '^ngModel'], + controller: 'UibDatepickerController', + controllerAs: 'datepicker', link: function(scope, element, attrs, ctrls) { var datepickerCtrl = ctrls[0], ngModelCtrl = ctrls[1]; - if ( ngModelCtrl ) { - datepickerCtrl.init( ngModelCtrl ); - } + datepickerCtrl.init(ngModelCtrl); } }; }) -.directive('daypicker', ['dateFilter', function (dateFilter) { +.directive('uibDaypicker', function() { return { - restrict: 'EA', replace: true, - templateUrl: 'template/datepicker/day.html', - require: '^datepicker', - link: function(scope, element, attrs, ctrl) { - scope.showWeeks = ctrl.showWeeks; - - ctrl.step = { months: 1 }; - ctrl.element = element; - - var DAYS_IN_MONTH = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; - function getDaysInMonth( year, month ) { - return ((month === 1) && (year % 4 === 0) && ((year % 100 !== 0) || (year % 400 === 0))) ? 29 : DAYS_IN_MONTH[month]; - } - - function getDates(startDate, n) { - var dates = new Array(n), current = new Date(startDate), i = 0; - current.setHours(12); // Prevent repeated dates because of timezone bug - while ( i < n ) { - dates[i++] = new Date(current); - current.setDate( current.getDate() + 1 ); - } - return dates; - } - - ctrl._refreshView = function() { - var year = ctrl.activeDate.getFullYear(), - month = ctrl.activeDate.getMonth(), - firstDayOfMonth = new Date(year, month, 1), - difference = ctrl.startingDay - firstDayOfMonth.getDay(), - numDisplayedFromPreviousMonth = (difference > 0) ? 7 - difference : - difference, - firstDate = new Date(firstDayOfMonth); - - if ( numDisplayedFromPreviousMonth > 0 ) { - firstDate.setDate( - numDisplayedFromPreviousMonth + 1 ); - } - - // 42 is the number of days on a six-month calendar - var days = getDates(firstDate, 42); - for (var i = 0; i < 42; i ++) { - days[i] = angular.extend(ctrl.createDateObject(days[i], ctrl.formatDay), { - secondary: days[i].getMonth() !== month, - uid: scope.uniqueId + '-' + i - }); - } - - scope.labels = new Array(7); - for (var j = 0; j < 7; j++) { - scope.labels[j] = { - abbr: dateFilter(days[j].date, ctrl.formatDayHeader), - full: dateFilter(days[j].date, 'EEEE') - }; - } - - scope.title = dateFilter(ctrl.activeDate, ctrl.formatDayTitle); - scope.rows = ctrl.split(days, 7); - - if ( scope.showWeeks ) { - scope.weekNumbers = []; - var weekNumber = getISO8601WeekNumber( scope.rows[0][0].date ), - numWeeks = scope.rows.length; - while( scope.weekNumbers.push(weekNumber++) < numWeeks ) {} - } - }; - - ctrl.compare = function(date1, date2) { - return (new Date( date1.getFullYear(), date1.getMonth(), date1.getDate() ) - new Date( date2.getFullYear(), date2.getMonth(), date2.getDate() ) ); - }; - - function getISO8601WeekNumber(date) { - var checkDate = new Date(date); - checkDate.setDate(checkDate.getDate() + 4 - (checkDate.getDay() || 7)); // Thursday - var time = checkDate.getTime(); - checkDate.setMonth(0); // Compare with Jan 1 - checkDate.setDate(1); - return Math.floor(Math.round((time - checkDate) / 86400000) / 7) + 1; - } - - ctrl.handleKeyDown = function( key, evt ) { - var date = ctrl.activeDate.getDate(); - - if (key === 'left') { - date = date - 1; // up - } else if (key === 'up') { - date = date - 7; // down - } else if (key === 'right') { - date = date + 1; // down - } else if (key === 'down') { - date = date + 7; - } else if (key === 'pageup' || key === 'pagedown') { - var month = ctrl.activeDate.getMonth() + (key === 'pageup' ? - 1 : 1); - ctrl.activeDate.setMonth(month, 1); - date = Math.min(getDaysInMonth(ctrl.activeDate.getFullYear(), ctrl.activeDate.getMonth()), date); - } else if (key === 'home') { - date = 1; - } else if (key === 'end') { - date = getDaysInMonth(ctrl.activeDate.getFullYear(), ctrl.activeDate.getMonth()); - } - ctrl.activeDate.setDate(date); - }; - - ctrl.refreshView(); - } - }; -}]) - -.directive('monthpicker', ['dateFilter', function (dateFilter) { - return { - restrict: 'EA', - replace: true, - templateUrl: 'template/datepicker/month.html', - require: '^datepicker', - link: function(scope, element, attrs, ctrl) { - ctrl.step = { years: 1 }; - ctrl.element = element; - - ctrl._refreshView = function() { - var months = new Array(12), - year = ctrl.activeDate.getFullYear(); - - for ( var i = 0; i < 12; i++ ) { - months[i] = angular.extend(ctrl.createDateObject(new Date(year, i, 1), ctrl.formatMonth), { - uid: scope.uniqueId + '-' + i - }); - } - - scope.title = dateFilter(ctrl.activeDate, ctrl.formatMonthTitle); - scope.rows = ctrl.split(months, 3); - }; - - ctrl.compare = function(date1, date2) { - return new Date( date1.getFullYear(), date1.getMonth() ) - new Date( date2.getFullYear(), date2.getMonth() ); - }; - - ctrl.handleKeyDown = function( key, evt ) { - var date = ctrl.activeDate.getMonth(); - - if (key === 'left') { - date = date - 1; // up - } else if (key === 'up') { - date = date - 3; // down - } else if (key === 'right') { - date = date + 1; // down - } else if (key === 'down') { - date = date + 3; - } else if (key === 'pageup' || key === 'pagedown') { - var year = ctrl.activeDate.getFullYear() + (key === 'pageup' ? - 1 : 1); - ctrl.activeDate.setFullYear(year); - } else if (key === 'home') { - date = 0; - } else if (key === 'end') { - date = 11; - } - ctrl.activeDate.setMonth(date); - }; - - ctrl.refreshView(); - } - }; -}]) - -.directive('yearpicker', ['dateFilter', function (dateFilter) { - return { - restrict: 'EA', - replace: true, - templateUrl: 'template/datepicker/year.html', - require: '^datepicker', - link: function(scope, element, attrs, ctrl) { - var range = ctrl.yearRange; - - ctrl.step = { years: range }; - ctrl.element = element; - - function getStartingYear( year ) { - return parseInt((year - 1) / range, 10) * range + 1; - } - - ctrl._refreshView = function() { - var years = new Array(range); - - for ( var i = 0, start = getStartingYear(ctrl.activeDate.getFullYear()); i < range; i++ ) { - years[i] = angular.extend(ctrl.createDateObject(new Date(start + i, 0, 1), ctrl.formatYear), { - uid: scope.uniqueId + '-' + i - }); - } - - scope.title = [years[0].label, years[range - 1].label].join(' - '); - scope.rows = ctrl.split(years, 5); - }; - - ctrl.compare = function(date1, date2) { - return date1.getFullYear() - date2.getFullYear(); - }; - - ctrl.handleKeyDown = function( key, evt ) { - var date = ctrl.activeDate.getFullYear(); - - if (key === 'left') { - date = date - 1; // up - } else if (key === 'up') { - date = date - 5; // down - } else if (key === 'right') { - date = date + 1; // down - } else if (key === 'down') { - date = date + 5; - } else if (key === 'pageup' || key === 'pagedown') { - date += (key === 'pageup' ? - 1 : 1) * ctrl.step.years; - } else if (key === 'home') { - date = getStartingYear( ctrl.activeDate.getFullYear() ); - } else if (key === 'end') { - date = getStartingYear( ctrl.activeDate.getFullYear() ) + range - 1; - } - ctrl.activeDate.setFullYear(date); - }; - - ctrl.refreshView(); - } - }; -}]) - -.constant('datepickerPopupConfig', { - datepickerPopup: 'yyyy-MM-dd', - currentText: 'Today', - clearText: 'Clear', - closeText: 'Done', - closeOnDateSelection: true, - appendToBody: false, - showButtonBar: true -}) - -.directive('datepickerPopup', ['$compile', '$parse', '$document', '$position', 'dateFilter', 'dateParser', 'datepickerPopupConfig', -function ($compile, $parse, $document, $position, dateFilter, dateParser, datepickerPopupConfig) { - return { - restrict: 'EA', - require: 'ngModel', - scope: { - isOpen: '=?', - currentText: '@', - clearText: '@', - closeText: '@', - dateDisabled: '&' + templateUrl: function(element, attrs) { + return attrs.templateUrl || 'uib/template/datepicker/day.html'; }, - link: function(scope, element, attrs, ngModel) { - var dateFormat, - closeOnDateSelection = angular.isDefined(attrs.closeOnDateSelection) ? scope.$parent.$eval(attrs.closeOnDateSelection) : datepickerPopupConfig.closeOnDateSelection, - appendToBody = angular.isDefined(attrs.datepickerAppendToBody) ? scope.$parent.$eval(attrs.datepickerAppendToBody) : datepickerPopupConfig.appendToBody; + require: ['^uibDatepicker', 'uibDaypicker'], + controller: 'UibDaypickerController', + link: function(scope, element, attrs, ctrls) { + var datepickerCtrl = ctrls[0], + daypickerCtrl = ctrls[1]; - scope.showButtonBar = angular.isDefined(attrs.showButtonBar) ? scope.$parent.$eval(attrs.showButtonBar) : datepickerPopupConfig.showButtonBar; - - scope.getText = function( key ) { - return scope[key + 'Text'] || datepickerPopupConfig[key + 'Text']; - }; - - attrs.$observe('datepickerPopup', function(value) { - dateFormat = value || datepickerPopupConfig.datepickerPopup; - ngModel.$render(); - }); - - // popup element used to display calendar - var popupEl = angular.element('
        '); - popupEl.attr({ - 'ng-model': 'date', - 'ng-change': 'dateSelection()' - }); - - function cameltoDash( string ){ - return string.replace(/([A-Z])/g, function($1) { return '-' + $1.toLowerCase(); }); - } - - // datepicker element - var datepickerEl = angular.element(popupEl.children()[0]); - if ( attrs.datepickerOptions ) { - angular.forEach(scope.$parent.$eval(attrs.datepickerOptions), function( value, option ) { - datepickerEl.attr( cameltoDash(option), value ); - }); - } - - angular.forEach(['minDate', 'maxDate'], function( key ) { - if ( attrs[key] ) { - scope.$parent.$watch($parse(attrs[key]), function(value){ - scope[key] = value; - }); - datepickerEl.attr(cameltoDash(key), key); - } - }); - if (attrs.dateDisabled) { - datepickerEl.attr('date-disabled', 'dateDisabled({ date: date, mode: mode })'); - } - - function parseDate(viewValue) { - if (!viewValue) { - ngModel.$setValidity('date', true); - return null; - } else if (angular.isDate(viewValue) && !isNaN(viewValue)) { - ngModel.$setValidity('date', true); - return viewValue; - } else if (angular.isString(viewValue)) { - var date = dateParser.parse(viewValue, dateFormat) || new Date(viewValue); - if (isNaN(date)) { - ngModel.$setValidity('date', false); - return undefined; - } else { - ngModel.$setValidity('date', true); - return date; - } - } else { - ngModel.$setValidity('date', false); - return undefined; - } - } - ngModel.$parsers.unshift(parseDate); - - // Inner change - scope.dateSelection = function(dt) { - if (angular.isDefined(dt)) { - scope.date = dt; - } - ngModel.$setViewValue(scope.date); - ngModel.$render(); - - if ( closeOnDateSelection ) { - scope.isOpen = false; - element[0].focus(); - } - }; - - element.bind('input change keyup', function() { - scope.$apply(function() { - scope.date = ngModel.$modelValue; - }); - }); - - // Outter change - ngModel.$render = function() { - var date = ngModel.$viewValue ? dateFilter(ngModel.$viewValue, dateFormat) : ''; - element.val(date); - scope.date = parseDate( ngModel.$modelValue ); - }; - - var documentClickBind = function(event) { - if (scope.isOpen && event.target !== element[0]) { - scope.$apply(function() { - scope.isOpen = false; - }); - } - }; - - var keydown = function(evt, noApply) { - scope.keydown(evt); - }; - element.bind('keydown', keydown); - - scope.keydown = function(evt) { - if (evt.which === 27) { - evt.preventDefault(); - evt.stopPropagation(); - scope.close(); - } else if (evt.which === 40 && !scope.isOpen) { - scope.isOpen = true; - } - }; - - scope.$watch('isOpen', function(value) { - if (value) { - scope.$broadcast('datepicker.focus'); - scope.position = appendToBody ? $position.offset(element) : $position.position(element); - scope.position.top = scope.position.top + element.prop('offsetHeight'); - - $document.bind('click', documentClickBind); - } else { - $document.unbind('click', documentClickBind); - } - }); - - scope.select = function( date ) { - if (date === 'today') { - var today = new Date(); - if (angular.isDate(ngModel.$modelValue)) { - date = new Date(ngModel.$modelValue); - date.setFullYear(today.getFullYear(), today.getMonth(), today.getDate()); - } else { - date = new Date(today.setHours(0, 0, 0, 0)); - } - } - scope.dateSelection( date ); - }; - - scope.close = function() { - scope.isOpen = false; - element[0].focus(); - }; - - var $popup = $compile(popupEl)(scope); - if ( appendToBody ) { - $document.find('body').append($popup); - } else { - element.after($popup); - } - - scope.$on('$destroy', function() { - $popup.remove(); - element.unbind('keydown', keydown); - $document.unbind('click', documentClickBind); - }); + daypickerCtrl.init(datepickerCtrl); } }; -}]) +}) -.directive('datepickerPopupWrap', function() { +.directive('uibMonthpicker', function() { return { - restrict:'EA', replace: true, - transclude: true, - templateUrl: 'template/datepicker/popup.html', - link:function (scope, element, attrs) { - element.bind('click', function(event) { - event.preventDefault(); - event.stopPropagation(); - }); + templateUrl: function(element, attrs) { + return attrs.templateUrl || 'uib/template/datepicker/month.html'; + }, + require: ['^uibDatepicker', 'uibMonthpicker'], + controller: 'UibMonthpickerController', + link: function(scope, element, attrs, ctrls) { + var datepickerCtrl = ctrls[0], + monthpickerCtrl = ctrls[1]; + + monthpickerCtrl.init(datepickerCtrl); + } + }; +}) + +.directive('uibYearpicker', function() { + return { + replace: true, + templateUrl: function(element, attrs) { + return attrs.templateUrl || 'uib/template/datepicker/year.html'; + }, + require: ['^uibDatepicker', 'uibYearpicker'], + controller: 'UibYearpickerController', + link: function(scope, element, attrs, ctrls) { + var ctrl = ctrls[0]; + angular.extend(ctrl, ctrls[1]); + ctrl.yearpickerInit(); + + ctrl.refreshView(); } }; }); -angular.module('ui.bootstrap.dropdown', []) +angular.module('ui.bootstrap.position', []) -.constant('dropdownConfig', { +/** + * A set of utility methods for working with the DOM. + * It is meant to be used where we need to absolute-position elements in + * relation to another element (this is the case for tooltips, popovers, + * typeahead suggestions etc.). + */ + .factory('$uibPosition', ['$document', '$window', function($document, $window) { + /** + * Used by scrollbarWidth() function to cache scrollbar's width. + * Do not access this variable directly, use scrollbarWidth() instead. + */ + var SCROLLBAR_WIDTH; + /** + * scrollbar on body and html element in IE and Edge overlay + * content and should be considered 0 width. + */ + var BODY_SCROLLBAR_WIDTH; + var OVERFLOW_REGEX = { + normal: /(auto|scroll)/, + hidden: /(auto|scroll|hidden)/ + }; + var PLACEMENT_REGEX = { + auto: /\s?auto?\s?/i, + primary: /^(top|bottom|left|right)$/, + secondary: /^(top|bottom|left|right|center)$/, + vertical: /^(top|bottom)$/ + }; + var BODY_REGEX = /(HTML|BODY)/; + + return { + + /** + * Provides a raw DOM element from a jQuery/jQLite element. + * + * @param {element} elem - The element to convert. + * + * @returns {element} A HTML element. + */ + getRawNode: function(elem) { + return elem.nodeName ? elem : elem[0] || elem; + }, + + /** + * Provides a parsed number for a style property. Strips + * units and casts invalid numbers to 0. + * + * @param {string} value - The style value to parse. + * + * @returns {number} A valid number. + */ + parseStyle: function(value) { + value = parseFloat(value); + return isFinite(value) ? value : 0; + }, + + /** + * Provides the closest positioned ancestor. + * + * @param {element} element - The element to get the offest parent for. + * + * @returns {element} The closest positioned ancestor. + */ + offsetParent: function(elem) { + elem = this.getRawNode(elem); + + var offsetParent = elem.offsetParent || $document[0].documentElement; + + function isStaticPositioned(el) { + return ($window.getComputedStyle(el).position || 'static') === 'static'; + } + + while (offsetParent && offsetParent !== $document[0].documentElement && isStaticPositioned(offsetParent)) { + offsetParent = offsetParent.offsetParent; + } + + return offsetParent || $document[0].documentElement; + }, + + /** + * Provides the scrollbar width, concept from TWBS measureScrollbar() + * function in https://github.com/twbs/bootstrap/blob/master/js/modal.js + * In IE and Edge, scollbar on body and html element overlay and should + * return a width of 0. + * + * @returns {number} The width of the browser scollbar. + */ + scrollbarWidth: function(isBody) { + if (isBody) { + if (angular.isUndefined(BODY_SCROLLBAR_WIDTH)) { + var bodyElem = $document.find('body'); + bodyElem.addClass('uib-position-body-scrollbar-measure'); + BODY_SCROLLBAR_WIDTH = $window.innerWidth - bodyElem[0].clientWidth; + BODY_SCROLLBAR_WIDTH = isFinite(BODY_SCROLLBAR_WIDTH) ? BODY_SCROLLBAR_WIDTH : 0; + bodyElem.removeClass('uib-position-body-scrollbar-measure'); + } + return BODY_SCROLLBAR_WIDTH; + } + + if (angular.isUndefined(SCROLLBAR_WIDTH)) { + var scrollElem = angular.element('
        '); + $document.find('body').append(scrollElem); + SCROLLBAR_WIDTH = scrollElem[0].offsetWidth - scrollElem[0].clientWidth; + SCROLLBAR_WIDTH = isFinite(SCROLLBAR_WIDTH) ? SCROLLBAR_WIDTH : 0; + scrollElem.remove(); + } + + return SCROLLBAR_WIDTH; + }, + + /** + * Provides the padding required on an element to replace the scrollbar. + * + * @returns {object} An object with the following properties: + *
          + *
        • **scrollbarWidth**: the width of the scrollbar
        • + *
        • **widthOverflow**: whether the the width is overflowing
        • + *
        • **right**: the amount of right padding on the element needed to replace the scrollbar
        • + *
        • **rightOriginal**: the amount of right padding currently on the element
        • + *
        • **heightOverflow**: whether the the height is overflowing
        • + *
        • **bottom**: the amount of bottom padding on the element needed to replace the scrollbar
        • + *
        • **bottomOriginal**: the amount of bottom padding currently on the element
        • + *
        + */ + scrollbarPadding: function(elem) { + elem = this.getRawNode(elem); + + var elemStyle = $window.getComputedStyle(elem); + var paddingRight = this.parseStyle(elemStyle.paddingRight); + var paddingBottom = this.parseStyle(elemStyle.paddingBottom); + var scrollParent = this.scrollParent(elem, false, true); + var scrollbarWidth = this.scrollbarWidth(scrollParent, BODY_REGEX.test(scrollParent.tagName)); + + return { + scrollbarWidth: scrollbarWidth, + widthOverflow: scrollParent.scrollWidth > scrollParent.clientWidth, + right: paddingRight + scrollbarWidth, + originalRight: paddingRight, + heightOverflow: scrollParent.scrollHeight > scrollParent.clientHeight, + bottom: paddingBottom + scrollbarWidth, + originalBottom: paddingBottom + }; + }, + + /** + * Checks to see if the element is scrollable. + * + * @param {element} elem - The element to check. + * @param {boolean=} [includeHidden=false] - Should scroll style of 'hidden' be considered, + * default is false. + * + * @returns {boolean} Whether the element is scrollable. + */ + isScrollable: function(elem, includeHidden) { + elem = this.getRawNode(elem); + + var overflowRegex = includeHidden ? OVERFLOW_REGEX.hidden : OVERFLOW_REGEX.normal; + var elemStyle = $window.getComputedStyle(elem); + return overflowRegex.test(elemStyle.overflow + elemStyle.overflowY + elemStyle.overflowX); + }, + + /** + * Provides the closest scrollable ancestor. + * A port of the jQuery UI scrollParent method: + * https://github.com/jquery/jquery-ui/blob/master/ui/scroll-parent.js + * + * @param {element} elem - The element to find the scroll parent of. + * @param {boolean=} [includeHidden=false] - Should scroll style of 'hidden' be considered, + * default is false. + * @param {boolean=} [includeSelf=false] - Should the element being passed be + * included in the scrollable llokup. + * + * @returns {element} A HTML element. + */ + scrollParent: function(elem, includeHidden, includeSelf) { + elem = this.getRawNode(elem); + + var overflowRegex = includeHidden ? OVERFLOW_REGEX.hidden : OVERFLOW_REGEX.normal; + var documentEl = $document[0].documentElement; + var elemStyle = $window.getComputedStyle(elem); + if (includeSelf && overflowRegex.test(elemStyle.overflow + elemStyle.overflowY + elemStyle.overflowX)) { + return elem; + } + var excludeStatic = elemStyle.position === 'absolute'; + var scrollParent = elem.parentElement || documentEl; + + if (scrollParent === documentEl || elemStyle.position === 'fixed') { + return documentEl; + } + + while (scrollParent.parentElement && scrollParent !== documentEl) { + var spStyle = $window.getComputedStyle(scrollParent); + if (excludeStatic && spStyle.position !== 'static') { + excludeStatic = false; + } + + if (!excludeStatic && overflowRegex.test(spStyle.overflow + spStyle.overflowY + spStyle.overflowX)) { + break; + } + scrollParent = scrollParent.parentElement; + } + + return scrollParent; + }, + + /** + * Provides read-only equivalent of jQuery's position function: + * http://api.jquery.com/position/ - distance to closest positioned + * ancestor. Does not account for margins by default like jQuery position. + * + * @param {element} elem - The element to caclulate the position on. + * @param {boolean=} [includeMargins=false] - Should margins be accounted + * for, default is false. + * + * @returns {object} An object with the following properties: + *
          + *
        • **width**: the width of the element
        • + *
        • **height**: the height of the element
        • + *
        • **top**: distance to top edge of offset parent
        • + *
        • **left**: distance to left edge of offset parent
        • + *
        + */ + position: function(elem, includeMagins) { + elem = this.getRawNode(elem); + + var elemOffset = this.offset(elem); + if (includeMagins) { + var elemStyle = $window.getComputedStyle(elem); + elemOffset.top -= this.parseStyle(elemStyle.marginTop); + elemOffset.left -= this.parseStyle(elemStyle.marginLeft); + } + var parent = this.offsetParent(elem); + var parentOffset = {top: 0, left: 0}; + + if (parent !== $document[0].documentElement) { + parentOffset = this.offset(parent); + parentOffset.top += parent.clientTop - parent.scrollTop; + parentOffset.left += parent.clientLeft - parent.scrollLeft; + } + + return { + width: Math.round(angular.isNumber(elemOffset.width) ? elemOffset.width : elem.offsetWidth), + height: Math.round(angular.isNumber(elemOffset.height) ? elemOffset.height : elem.offsetHeight), + top: Math.round(elemOffset.top - parentOffset.top), + left: Math.round(elemOffset.left - parentOffset.left) + }; + }, + + /** + * Provides read-only equivalent of jQuery's offset function: + * http://api.jquery.com/offset/ - distance to viewport. Does + * not account for borders, margins, or padding on the body + * element. + * + * @param {element} elem - The element to calculate the offset on. + * + * @returns {object} An object with the following properties: + *
          + *
        • **width**: the width of the element
        • + *
        • **height**: the height of the element
        • + *
        • **top**: distance to top edge of viewport
        • + *
        • **right**: distance to bottom edge of viewport
        • + *
        + */ + offset: function(elem) { + elem = this.getRawNode(elem); + + var elemBCR = elem.getBoundingClientRect(); + return { + width: Math.round(angular.isNumber(elemBCR.width) ? elemBCR.width : elem.offsetWidth), + height: Math.round(angular.isNumber(elemBCR.height) ? elemBCR.height : elem.offsetHeight), + top: Math.round(elemBCR.top + ($window.pageYOffset || $document[0].documentElement.scrollTop)), + left: Math.round(elemBCR.left + ($window.pageXOffset || $document[0].documentElement.scrollLeft)) + }; + }, + + /** + * Provides offset distance to the closest scrollable ancestor + * or viewport. Accounts for border and scrollbar width. + * + * Right and bottom dimensions represent the distance to the + * respective edge of the viewport element. If the element + * edge extends beyond the viewport, a negative value will be + * reported. + * + * @param {element} elem - The element to get the viewport offset for. + * @param {boolean=} [useDocument=false] - Should the viewport be the document element instead + * of the first scrollable element, default is false. + * @param {boolean=} [includePadding=true] - Should the padding on the offset parent element + * be accounted for, default is true. + * + * @returns {object} An object with the following properties: + *
          + *
        • **top**: distance to the top content edge of viewport element
        • + *
        • **bottom**: distance to the bottom content edge of viewport element
        • + *
        • **left**: distance to the left content edge of viewport element
        • + *
        • **right**: distance to the right content edge of viewport element
        • + *
        + */ + viewportOffset: function(elem, useDocument, includePadding) { + elem = this.getRawNode(elem); + includePadding = includePadding !== false ? true : false; + + var elemBCR = elem.getBoundingClientRect(); + var offsetBCR = {top: 0, left: 0, bottom: 0, right: 0}; + + var offsetParent = useDocument ? $document[0].documentElement : this.scrollParent(elem); + var offsetParentBCR = offsetParent.getBoundingClientRect(); + + offsetBCR.top = offsetParentBCR.top + offsetParent.clientTop; + offsetBCR.left = offsetParentBCR.left + offsetParent.clientLeft; + if (offsetParent === $document[0].documentElement) { + offsetBCR.top += $window.pageYOffset; + offsetBCR.left += $window.pageXOffset; + } + offsetBCR.bottom = offsetBCR.top + offsetParent.clientHeight; + offsetBCR.right = offsetBCR.left + offsetParent.clientWidth; + + if (includePadding) { + var offsetParentStyle = $window.getComputedStyle(offsetParent); + offsetBCR.top += this.parseStyle(offsetParentStyle.paddingTop); + offsetBCR.bottom -= this.parseStyle(offsetParentStyle.paddingBottom); + offsetBCR.left += this.parseStyle(offsetParentStyle.paddingLeft); + offsetBCR.right -= this.parseStyle(offsetParentStyle.paddingRight); + } + + return { + top: Math.round(elemBCR.top - offsetBCR.top), + bottom: Math.round(offsetBCR.bottom - elemBCR.bottom), + left: Math.round(elemBCR.left - offsetBCR.left), + right: Math.round(offsetBCR.right - elemBCR.right) + }; + }, + + /** + * Provides an array of placement values parsed from a placement string. + * Along with the 'auto' indicator, supported placement strings are: + *
          + *
        • top: element on top, horizontally centered on host element.
        • + *
        • top-left: element on top, left edge aligned with host element left edge.
        • + *
        • top-right: element on top, lerightft edge aligned with host element right edge.
        • + *
        • bottom: element on bottom, horizontally centered on host element.
        • + *
        • bottom-left: element on bottom, left edge aligned with host element left edge.
        • + *
        • bottom-right: element on bottom, right edge aligned with host element right edge.
        • + *
        • left: element on left, vertically centered on host element.
        • + *
        • left-top: element on left, top edge aligned with host element top edge.
        • + *
        • left-bottom: element on left, bottom edge aligned with host element bottom edge.
        • + *
        • right: element on right, vertically centered on host element.
        • + *
        • right-top: element on right, top edge aligned with host element top edge.
        • + *
        • right-bottom: element on right, bottom edge aligned with host element bottom edge.
        • + *
        + * A placement string with an 'auto' indicator is expected to be + * space separated from the placement, i.e: 'auto bottom-left' If + * the primary and secondary placement values do not match 'top, + * bottom, left, right' then 'top' will be the primary placement and + * 'center' will be the secondary placement. If 'auto' is passed, true + * will be returned as the 3rd value of the array. + * + * @param {string} placement - The placement string to parse. + * + * @returns {array} An array with the following values + *
          + *
        • **[0]**: The primary placement.
        • + *
        • **[1]**: The secondary placement.
        • + *
        • **[2]**: If auto is passed: true, else undefined.
        • + *
        + */ + parsePlacement: function(placement) { + var autoPlace = PLACEMENT_REGEX.auto.test(placement); + if (autoPlace) { + placement = placement.replace(PLACEMENT_REGEX.auto, ''); + } + + placement = placement.split('-'); + + placement[0] = placement[0] || 'top'; + if (!PLACEMENT_REGEX.primary.test(placement[0])) { + placement[0] = 'top'; + } + + placement[1] = placement[1] || 'center'; + if (!PLACEMENT_REGEX.secondary.test(placement[1])) { + placement[1] = 'center'; + } + + if (autoPlace) { + placement[2] = true; + } else { + placement[2] = false; + } + + return placement; + }, + + /** + * Provides coordinates for an element to be positioned relative to + * another element. Passing 'auto' as part of the placement parameter + * will enable smart placement - where the element fits. i.e: + * 'auto left-top' will check to see if there is enough space to the left + * of the hostElem to fit the targetElem, if not place right (same for secondary + * top placement). Available space is calculated using the viewportOffset + * function. + * + * @param {element} hostElem - The element to position against. + * @param {element} targetElem - The element to position. + * @param {string=} [placement=top] - The placement for the targetElem, + * default is 'top'. 'center' is assumed as secondary placement for + * 'top', 'left', 'right', and 'bottom' placements. Available placements are: + *
          + *
        • top
        • + *
        • top-right
        • + *
        • top-left
        • + *
        • bottom
        • + *
        • bottom-left
        • + *
        • bottom-right
        • + *
        • left
        • + *
        • left-top
        • + *
        • left-bottom
        • + *
        • right
        • + *
        • right-top
        • + *
        • right-bottom
        • + *
        + * @param {boolean=} [appendToBody=false] - Should the top and left values returned + * be calculated from the body element, default is false. + * + * @returns {object} An object with the following properties: + *
          + *
        • **top**: Value for targetElem top.
        • + *
        • **left**: Value for targetElem left.
        • + *
        • **placement**: The resolved placement.
        • + *
        + */ + positionElements: function(hostElem, targetElem, placement, appendToBody) { + hostElem = this.getRawNode(hostElem); + targetElem = this.getRawNode(targetElem); + + // need to read from prop to support tests. + var targetWidth = angular.isDefined(targetElem.offsetWidth) ? targetElem.offsetWidth : targetElem.prop('offsetWidth'); + var targetHeight = angular.isDefined(targetElem.offsetHeight) ? targetElem.offsetHeight : targetElem.prop('offsetHeight'); + + placement = this.parsePlacement(placement); + + var hostElemPos = appendToBody ? this.offset(hostElem) : this.position(hostElem); + var targetElemPos = {top: 0, left: 0, placement: ''}; + + if (placement[2]) { + var viewportOffset = this.viewportOffset(hostElem, appendToBody); + + var targetElemStyle = $window.getComputedStyle(targetElem); + var adjustedSize = { + width: targetWidth + Math.round(Math.abs(this.parseStyle(targetElemStyle.marginLeft) + this.parseStyle(targetElemStyle.marginRight))), + height: targetHeight + Math.round(Math.abs(this.parseStyle(targetElemStyle.marginTop) + this.parseStyle(targetElemStyle.marginBottom))) + }; + + placement[0] = placement[0] === 'top' && adjustedSize.height > viewportOffset.top && adjustedSize.height <= viewportOffset.bottom ? 'bottom' : + placement[0] === 'bottom' && adjustedSize.height > viewportOffset.bottom && adjustedSize.height <= viewportOffset.top ? 'top' : + placement[0] === 'left' && adjustedSize.width > viewportOffset.left && adjustedSize.width <= viewportOffset.right ? 'right' : + placement[0] === 'right' && adjustedSize.width > viewportOffset.right && adjustedSize.width <= viewportOffset.left ? 'left' : + placement[0]; + + placement[1] = placement[1] === 'top' && adjustedSize.height - hostElemPos.height > viewportOffset.bottom && adjustedSize.height - hostElemPos.height <= viewportOffset.top ? 'bottom' : + placement[1] === 'bottom' && adjustedSize.height - hostElemPos.height > viewportOffset.top && adjustedSize.height - hostElemPos.height <= viewportOffset.bottom ? 'top' : + placement[1] === 'left' && adjustedSize.width - hostElemPos.width > viewportOffset.right && adjustedSize.width - hostElemPos.width <= viewportOffset.left ? 'right' : + placement[1] === 'right' && adjustedSize.width - hostElemPos.width > viewportOffset.left && adjustedSize.width - hostElemPos.width <= viewportOffset.right ? 'left' : + placement[1]; + + if (placement[1] === 'center') { + if (PLACEMENT_REGEX.vertical.test(placement[0])) { + var xOverflow = hostElemPos.width / 2 - targetWidth / 2; + if (viewportOffset.left + xOverflow < 0 && adjustedSize.width - hostElemPos.width <= viewportOffset.right) { + placement[1] = 'left'; + } else if (viewportOffset.right + xOverflow < 0 && adjustedSize.width - hostElemPos.width <= viewportOffset.left) { + placement[1] = 'right'; + } + } else { + var yOverflow = hostElemPos.height / 2 - adjustedSize.height / 2; + if (viewportOffset.top + yOverflow < 0 && adjustedSize.height - hostElemPos.height <= viewportOffset.bottom) { + placement[1] = 'top'; + } else if (viewportOffset.bottom + yOverflow < 0 && adjustedSize.height - hostElemPos.height <= viewportOffset.top) { + placement[1] = 'bottom'; + } + } + } + } + + switch (placement[0]) { + case 'top': + targetElemPos.top = hostElemPos.top - targetHeight; + break; + case 'bottom': + targetElemPos.top = hostElemPos.top + hostElemPos.height; + break; + case 'left': + targetElemPos.left = hostElemPos.left - targetWidth; + break; + case 'right': + targetElemPos.left = hostElemPos.left + hostElemPos.width; + break; + } + + switch (placement[1]) { + case 'top': + targetElemPos.top = hostElemPos.top; + break; + case 'bottom': + targetElemPos.top = hostElemPos.top + hostElemPos.height - targetHeight; + break; + case 'left': + targetElemPos.left = hostElemPos.left; + break; + case 'right': + targetElemPos.left = hostElemPos.left + hostElemPos.width - targetWidth; + break; + case 'center': + if (PLACEMENT_REGEX.vertical.test(placement[0])) { + targetElemPos.left = hostElemPos.left + hostElemPos.width / 2 - targetWidth / 2; + } else { + targetElemPos.top = hostElemPos.top + hostElemPos.height / 2 - targetHeight / 2; + } + break; + } + + targetElemPos.top = Math.round(targetElemPos.top); + targetElemPos.left = Math.round(targetElemPos.left); + targetElemPos.placement = placement[1] === 'center' ? placement[0] : placement[0] + '-' + placement[1]; + + return targetElemPos; + }, + + /** + * Provides a way for positioning tooltip & dropdown + * arrows when using placement options beyond the standard + * left, right, top, or bottom. + * + * @param {element} elem - The tooltip/dropdown element. + * @param {string} placement - The placement for the elem. + */ + positionArrow: function(elem, placement) { + elem = this.getRawNode(elem); + + var innerElem = elem.querySelector('.tooltip-inner, .popover-inner'); + if (!innerElem) { + return; + } + + var isTooltip = angular.element(innerElem).hasClass('tooltip-inner'); + + var arrowElem = isTooltip ? elem.querySelector('.tooltip-arrow') : elem.querySelector('.arrow'); + if (!arrowElem) { + return; + } + + var arrowCss = { + top: '', + bottom: '', + left: '', + right: '' + }; + + placement = this.parsePlacement(placement); + if (placement[1] === 'center') { + // no adjustment necessary - just reset styles + angular.element(arrowElem).css(arrowCss); + return; + } + + var borderProp = 'border-' + placement[0] + '-width'; + var borderWidth = $window.getComputedStyle(arrowElem)[borderProp]; + + var borderRadiusProp = 'border-'; + if (PLACEMENT_REGEX.vertical.test(placement[0])) { + borderRadiusProp += placement[0] + '-' + placement[1]; + } else { + borderRadiusProp += placement[1] + '-' + placement[0]; + } + borderRadiusProp += '-radius'; + var borderRadius = $window.getComputedStyle(isTooltip ? innerElem : elem)[borderRadiusProp]; + + switch (placement[0]) { + case 'top': + arrowCss.bottom = isTooltip ? '0' : '-' + borderWidth; + break; + case 'bottom': + arrowCss.top = isTooltip ? '0' : '-' + borderWidth; + break; + case 'left': + arrowCss.right = isTooltip ? '0' : '-' + borderWidth; + break; + case 'right': + arrowCss.left = isTooltip ? '0' : '-' + borderWidth; + break; + } + + arrowCss[placement[1]] = borderRadius; + + angular.element(arrowElem).css(arrowCss); + } + }; + }]); + +angular.module('ui.bootstrap.datepickerPopup', ['ui.bootstrap.datepicker', 'ui.bootstrap.position']) + +.value('$datepickerPopupLiteralWarning', true) + +.constant('uibDatepickerPopupConfig', { + altInputFormats: [], + appendToBody: false, + clearText: 'Clear', + closeOnDateSelection: true, + closeText: 'Done', + currentText: 'Today', + datepickerPopup: 'yyyy-MM-dd', + datepickerPopupTemplateUrl: 'uib/template/datepickerPopup/popup.html', + datepickerTemplateUrl: 'uib/template/datepicker/datepicker.html', + html5Types: { + date: 'yyyy-MM-dd', + 'datetime-local': 'yyyy-MM-ddTHH:mm:ss.sss', + 'month': 'yyyy-MM' + }, + onOpenFocus: true, + showButtonBar: true, + placement: 'auto bottom-left' +}) + +.controller('UibDatepickerPopupController', ['$scope', '$element', '$attrs', '$compile', '$log', '$parse', '$window', '$document', '$rootScope', '$uibPosition', 'dateFilter', 'uibDateParser', 'uibDatepickerPopupConfig', '$timeout', 'uibDatepickerConfig', '$datepickerPopupLiteralWarning', +function($scope, $element, $attrs, $compile, $log, $parse, $window, $document, $rootScope, $position, dateFilter, dateParser, datepickerPopupConfig, $timeout, datepickerConfig, $datepickerPopupLiteralWarning) { + var cache = {}, + isHtml5DateInput = false; + var dateFormat, closeOnDateSelection, appendToBody, onOpenFocus, + datepickerPopupTemplateUrl, datepickerTemplateUrl, popupEl, datepickerEl, scrollParentEl, + ngModel, ngModelOptions, $popup, altInputFormats, watchListeners = [], + timezone; + + this.init = function(_ngModel_) { + ngModel = _ngModel_; + ngModelOptions = _ngModel_.$options; + closeOnDateSelection = angular.isDefined($attrs.closeOnDateSelection) ? + $scope.$parent.$eval($attrs.closeOnDateSelection) : + datepickerPopupConfig.closeOnDateSelection; + appendToBody = angular.isDefined($attrs.datepickerAppendToBody) ? + $scope.$parent.$eval($attrs.datepickerAppendToBody) : + datepickerPopupConfig.appendToBody; + onOpenFocus = angular.isDefined($attrs.onOpenFocus) ? + $scope.$parent.$eval($attrs.onOpenFocus) : datepickerPopupConfig.onOpenFocus; + datepickerPopupTemplateUrl = angular.isDefined($attrs.datepickerPopupTemplateUrl) ? + $attrs.datepickerPopupTemplateUrl : + datepickerPopupConfig.datepickerPopupTemplateUrl; + datepickerTemplateUrl = angular.isDefined($attrs.datepickerTemplateUrl) ? + $attrs.datepickerTemplateUrl : datepickerPopupConfig.datepickerTemplateUrl; + altInputFormats = angular.isDefined($attrs.altInputFormats) ? + $scope.$parent.$eval($attrs.altInputFormats) : + datepickerPopupConfig.altInputFormats; + + $scope.showButtonBar = angular.isDefined($attrs.showButtonBar) ? + $scope.$parent.$eval($attrs.showButtonBar) : + datepickerPopupConfig.showButtonBar; + + if (datepickerPopupConfig.html5Types[$attrs.type]) { + dateFormat = datepickerPopupConfig.html5Types[$attrs.type]; + isHtml5DateInput = true; + } else { + dateFormat = $attrs.uibDatepickerPopup || datepickerPopupConfig.datepickerPopup; + $attrs.$observe('uibDatepickerPopup', function(value, oldValue) { + var newDateFormat = value || datepickerPopupConfig.datepickerPopup; + // Invalidate the $modelValue to ensure that formatters re-run + // FIXME: Refactor when PR is merged: https://github.com/angular/angular.js/pull/10764 + if (newDateFormat !== dateFormat) { + dateFormat = newDateFormat; + ngModel.$modelValue = null; + + if (!dateFormat) { + throw new Error('uibDatepickerPopup must have a date format specified.'); + } + } + }); + } + + if (!dateFormat) { + throw new Error('uibDatepickerPopup must have a date format specified.'); + } + + if (isHtml5DateInput && $attrs.uibDatepickerPopup) { + throw new Error('HTML5 date input types do not support custom formats.'); + } + + // popup element used to display calendar + popupEl = angular.element('
        '); + if (ngModelOptions) { + timezone = ngModelOptions.timezone; + $scope.ngModelOptions = angular.copy(ngModelOptions); + $scope.ngModelOptions.timezone = null; + if ($scope.ngModelOptions.updateOnDefault === true) { + $scope.ngModelOptions.updateOn = $scope.ngModelOptions.updateOn ? + $scope.ngModelOptions.updateOn + ' default' : 'default'; + } + + popupEl.attr('ng-model-options', 'ngModelOptions'); + } else { + timezone = null; + } + + popupEl.attr({ + 'ng-model': 'date', + 'ng-change': 'dateSelection(date)', + 'template-url': datepickerPopupTemplateUrl + }); + + // datepicker element + datepickerEl = angular.element(popupEl.children()[0]); + datepickerEl.attr('template-url', datepickerTemplateUrl); + + if (!$scope.datepickerOptions) { + $scope.datepickerOptions = {}; + } + + if (isHtml5DateInput) { + if ($attrs.type === 'month') { + $scope.datepickerOptions.datepickerMode = 'month'; + $scope.datepickerOptions.minMode = 'month'; + } + } + + datepickerEl.attr('datepicker-options', 'datepickerOptions'); + + if (!isHtml5DateInput) { + // Internal API to maintain the correct ng-invalid-[key] class + ngModel.$$parserName = 'date'; + ngModel.$validators.date = validator; + ngModel.$parsers.unshift(parseDate); + ngModel.$formatters.push(function(value) { + if (ngModel.$isEmpty(value)) { + $scope.date = value; + return value; + } + + $scope.date = dateParser.fromTimezone(value, timezone); + + if (angular.isNumber($scope.date)) { + $scope.date = new Date($scope.date); + } + + return dateParser.filter($scope.date, dateFormat); + }); + } else { + ngModel.$formatters.push(function(value) { + $scope.date = dateParser.fromTimezone(value, timezone); + return value; + }); + } + + // Detect changes in the view from the text box + ngModel.$viewChangeListeners.push(function() { + $scope.date = parseDateString(ngModel.$viewValue); + }); + + $element.on('keydown', inputKeydownBind); + + $popup = $compile(popupEl)($scope); + // Prevent jQuery cache memory leak (template is now redundant after linking) + popupEl.remove(); + + if (appendToBody) { + $document.find('body').append($popup); + } else { + $element.after($popup); + } + + $scope.$on('$destroy', function() { + if ($scope.isOpen === true) { + if (!$rootScope.$$phase) { + $scope.$apply(function() { + $scope.isOpen = false; + }); + } + } + + $popup.remove(); + $element.off('keydown', inputKeydownBind); + $document.off('click', documentClickBind); + if (scrollParentEl) { + scrollParentEl.off('scroll', positionPopup); + } + angular.element($window).off('resize', positionPopup); + + //Clear all watch listeners on destroy + while (watchListeners.length) { + watchListeners.shift()(); + } + }); + }; + + $scope.getText = function(key) { + return $scope[key + 'Text'] || datepickerPopupConfig[key + 'Text']; + }; + + $scope.isDisabled = function(date) { + if (date === 'today') { + date = dateParser.fromTimezone(new Date(), timezone); + } + + var dates = {}; + angular.forEach(['minDate', 'maxDate'], function(key) { + if (!$scope.datepickerOptions[key]) { + dates[key] = null; + } else if (angular.isDate($scope.datepickerOptions[key])) { + dates[key] = dateParser.fromTimezone(new Date($scope.datepickerOptions[key]), timezone); + } else { + if ($datepickerPopupLiteralWarning) { + $log.warn('Literal date support has been deprecated, please switch to date object usage'); + } + + dates[key] = new Date(dateFilter($scope.datepickerOptions[key], 'medium')); + } + }); + + return $scope.datepickerOptions && + dates.minDate && $scope.compare(date, dates.minDate) < 0 || + dates.maxDate && $scope.compare(date, dates.maxDate) > 0; + }; + + $scope.compare = function(date1, date2) { + return new Date(date1.getFullYear(), date1.getMonth(), date1.getDate()) - new Date(date2.getFullYear(), date2.getMonth(), date2.getDate()); + }; + + // Inner change + $scope.dateSelection = function(dt) { + if (angular.isDefined(dt)) { + $scope.date = dt; + } + var date = $scope.date ? dateParser.filter($scope.date, dateFormat) : null; // Setting to NULL is necessary for form validators to function + $element.val(date); + ngModel.$setViewValue(date); + + if (closeOnDateSelection) { + $scope.isOpen = false; + $element[0].focus(); + } + }; + + $scope.keydown = function(evt) { + if (evt.which === 27) { + evt.stopPropagation(); + $scope.isOpen = false; + $element[0].focus(); + } + }; + + $scope.select = function(date, evt) { + evt.stopPropagation(); + + if (date === 'today') { + var today = new Date(); + if (angular.isDate($scope.date)) { + date = new Date($scope.date); + date.setFullYear(today.getFullYear(), today.getMonth(), today.getDate()); + } else { + date = new Date(today.setHours(0, 0, 0, 0)); + } + } + $scope.dateSelection(date); + }; + + $scope.close = function(evt) { + evt.stopPropagation(); + + $scope.isOpen = false; + $element[0].focus(); + }; + + $scope.disabled = angular.isDefined($attrs.disabled) || false; + if ($attrs.ngDisabled) { + watchListeners.push($scope.$parent.$watch($parse($attrs.ngDisabled), function(disabled) { + $scope.disabled = disabled; + })); + } + + $scope.$watch('isOpen', function(value) { + if (value) { + if (!$scope.disabled) { + $timeout(function() { + positionPopup(); + + if (onOpenFocus) { + $scope.$broadcast('uib:datepicker.focus'); + } + + $document.on('click', documentClickBind); + + var placement = $attrs.popupPlacement ? $attrs.popupPlacement : datepickerPopupConfig.placement; + if (appendToBody || $position.parsePlacement(placement)[2]) { + scrollParentEl = scrollParentEl || angular.element($position.scrollParent($element)); + if (scrollParentEl) { + scrollParentEl.on('scroll', positionPopup); + } + } else { + scrollParentEl = null; + } + + angular.element($window).on('resize', positionPopup); + }, 0, false); + } else { + $scope.isOpen = false; + } + } else { + $document.off('click', documentClickBind); + if (scrollParentEl) { + scrollParentEl.off('scroll', positionPopup); + } + angular.element($window).off('resize', positionPopup); + } + }); + + function cameltoDash(string) { + return string.replace(/([A-Z])/g, function($1) { return '-' + $1.toLowerCase(); }); + } + + function parseDateString(viewValue) { + var date = dateParser.parse(viewValue, dateFormat, $scope.date); + if (isNaN(date)) { + for (var i = 0; i < altInputFormats.length; i++) { + date = dateParser.parse(viewValue, altInputFormats[i], $scope.date); + if (!isNaN(date)) { + return date; + } + } + } + return date; + } + + function parseDate(viewValue) { + if (angular.isNumber(viewValue)) { + // presumably timestamp to date object + viewValue = new Date(viewValue); + } + + if (!viewValue) { + return null; + } + + if (angular.isDate(viewValue) && !isNaN(viewValue)) { + return viewValue; + } + + if (angular.isString(viewValue)) { + var date = parseDateString(viewValue); + if (!isNaN(date)) { + return dateParser.toTimezone(date, timezone); + } + } + + return ngModel.$options && ngModel.$options.allowInvalid ? viewValue : undefined; + } + + function validator(modelValue, viewValue) { + var value = modelValue || viewValue; + + if (!$attrs.ngRequired && !value) { + return true; + } + + if (angular.isNumber(value)) { + value = new Date(value); + } + + if (!value) { + return true; + } + + if (angular.isDate(value) && !isNaN(value)) { + return true; + } + + if (angular.isString(value)) { + return !isNaN(parseDateString(viewValue)); + } + + return false; + } + + function documentClickBind(event) { + if (!$scope.isOpen && $scope.disabled) { + return; + } + + var popup = $popup[0]; + var dpContainsTarget = $element[0].contains(event.target); + // The popup node may not be an element node + // In some browsers (IE) only element nodes have the 'contains' function + var popupContainsTarget = popup.contains !== undefined && popup.contains(event.target); + if ($scope.isOpen && !(dpContainsTarget || popupContainsTarget)) { + $scope.$apply(function() { + $scope.isOpen = false; + }); + } + } + + function inputKeydownBind(evt) { + if (evt.which === 27 && $scope.isOpen) { + evt.preventDefault(); + evt.stopPropagation(); + $scope.$apply(function() { + $scope.isOpen = false; + }); + $element[0].focus(); + } else if (evt.which === 40 && !$scope.isOpen) { + evt.preventDefault(); + evt.stopPropagation(); + $scope.$apply(function() { + $scope.isOpen = true; + }); + } + } + + function positionPopup() { + if ($scope.isOpen) { + var dpElement = angular.element($popup[0].querySelector('.uib-datepicker-popup')); + var placement = $attrs.popupPlacement ? $attrs.popupPlacement : datepickerPopupConfig.placement; + var position = $position.positionElements($element, dpElement, placement, appendToBody); + dpElement.css({top: position.top + 'px', left: position.left + 'px'}); + if (dpElement.hasClass('uib-position-measure')) { + dpElement.removeClass('uib-position-measure'); + } + } + } + + $scope.$on('uib:datepicker.mode', function() { + $timeout(positionPopup, 0, false); + }); +}]) + +.directive('uibDatepickerPopup', function() { + return { + require: ['ngModel', 'uibDatepickerPopup'], + controller: 'UibDatepickerPopupController', + scope: { + datepickerOptions: '=?', + isOpen: '=?', + currentText: '@', + clearText: '@', + closeText: '@' + }, + link: function(scope, element, attrs, ctrls) { + var ngModel = ctrls[0], + ctrl = ctrls[1]; + + ctrl.init(ngModel); + } + }; +}) + +.directive('uibDatepickerPopupWrap', function() { + return { + replace: true, + transclude: true, + templateUrl: function(element, attrs) { + return attrs.templateUrl || 'uib/template/datepickerPopup/popup.html'; + } + }; +}); + +angular.module('ui.bootstrap.debounce', []) +/** + * A helper, internal service that debounces a function + */ + .factory('$$debounce', ['$timeout', function($timeout) { + return function(callback, debounceTime) { + var timeoutPromise; + + return function() { + var self = this; + var args = Array.prototype.slice.call(arguments); + if (timeoutPromise) { + $timeout.cancel(timeoutPromise); + } + + timeoutPromise = $timeout(function() { + callback.apply(self, args); + }, debounceTime); + }; + }; + }]); + +angular.module('ui.bootstrap.dropdown', ['ui.bootstrap.position']) + +.constant('uibDropdownConfig', { + appendToOpenClass: 'uib-dropdown-open', openClass: 'open' }) -.service('dropdownService', ['$document', function($document) { +.service('uibDropdownService', ['$document', '$rootScope', function($document, $rootScope) { var openScope = null; - this.open = function( dropdownScope ) { - if ( !openScope ) { - $document.bind('click', closeDropdown); - $document.bind('keydown', escapeKeyBind); + this.open = function(dropdownScope, element) { + if (!openScope) { + $document.on('click', closeDropdown); + element.on('keydown', keybindFilter); } - if ( openScope && openScope !== dropdownScope ) { - openScope.isOpen = false; + if (openScope && openScope !== dropdownScope) { + openScope.isOpen = false; } openScope = dropdownScope; }; - this.close = function( dropdownScope ) { - if ( openScope === dropdownScope ) { + this.close = function(dropdownScope, element) { + if (openScope === dropdownScope) { openScope = null; - $document.unbind('click', closeDropdown); - $document.unbind('keydown', escapeKeyBind); + $document.off('click', closeDropdown); + element.off('keydown', keybindFilter); } }; - var closeDropdown = function( evt ) { - if (evt && evt.isDefaultPrevented()) { - return; + var closeDropdown = function(evt) { + // This method may still be called during the same mouse event that + // unbound this event handler. So check openScope before proceeding. + if (!openScope) { return; } + + if (evt && openScope.getAutoClose() === 'disabled') { return; } + + if (evt && evt.which === 3) { return; } + + var toggleElement = openScope.getToggleElement(); + if (evt && toggleElement && toggleElement[0].contains(evt.target)) { + return; } - openScope.$apply(function() { - openScope.isOpen = false; - }); + var dropdownElement = openScope.getDropdownElement(); + if (evt && openScope.getAutoClose() === 'outsideClick' && + dropdownElement && dropdownElement[0].contains(evt.target)) { + return; + } + + openScope.isOpen = false; + + if (!$rootScope.$$phase) { + openScope.$apply(); + } }; - var escapeKeyBind = function( evt ) { - if ( evt.which === 27 ) { + var keybindFilter = function(evt) { + if (evt.which === 27) { + evt.stopPropagation(); openScope.focusToggleElement(); closeDropdown(); + } else if (openScope.isKeynavEnabled() && [38, 40].indexOf(evt.which) !== -1 && openScope.isOpen) { + evt.preventDefault(); + evt.stopPropagation(); + openScope.focusDropdownEntry(evt.which); } }; }]) -.controller('DropdownController', ['$scope', '$attrs', '$parse', 'dropdownConfig', 'dropdownService', '$animate', function($scope, $attrs, $parse, dropdownConfig, dropdownService, $animate) { +.controller('UibDropdownController', ['$scope', '$element', '$attrs', '$parse', 'uibDropdownConfig', 'uibDropdownService', '$animate', '$uibPosition', '$document', '$compile', '$templateRequest', function($scope, $element, $attrs, $parse, dropdownConfig, uibDropdownService, $animate, $position, $document, $compile, $templateRequest) { var self = this, - scope = $scope.$new(), // create a child scope so we are not polluting original one - openClass = dropdownConfig.openClass, - getIsOpen, - setIsOpen = angular.noop, - toggleInvoker = $attrs.onToggle ? $parse($attrs.onToggle) : angular.noop; + scope = $scope.$new(), // create a child scope so we are not polluting original one + templateScope, + appendToOpenClass = dropdownConfig.appendToOpenClass, + openClass = dropdownConfig.openClass, + getIsOpen, + setIsOpen = angular.noop, + toggleInvoker = $attrs.onToggle ? $parse($attrs.onToggle) : angular.noop, + appendToBody = false, + appendTo = null, + keynavEnabled = false, + selectedOption = null, + body = $document.find('body'); - this.init = function( element ) { - self.$element = element; + $element.addClass('dropdown'); - if ( $attrs.isOpen ) { + this.init = function() { + if ($attrs.isOpen) { getIsOpen = $parse($attrs.isOpen); setIsOpen = getIsOpen.assign; @@ -1667,10 +3120,36 @@ angular.module('ui.bootstrap.dropdown', []) scope.isOpen = !!value; }); } + + if (angular.isDefined($attrs.dropdownAppendTo)) { + var appendToEl = $parse($attrs.dropdownAppendTo)(scope); + if (appendToEl) { + appendTo = angular.element(appendToEl); + } + } + + appendToBody = angular.isDefined($attrs.dropdownAppendToBody); + keynavEnabled = angular.isDefined($attrs.keyboardNav); + + if (appendToBody && !appendTo) { + appendTo = body; + } + + if (appendTo && self.dropdownMenu) { + appendTo.append(self.dropdownMenu); + $element.on('$destroy', function handleDestroyEvent() { + self.dropdownMenu.remove(); + }); + } }; - this.toggle = function( open ) { - return scope.isOpen = arguments.length ? !!open : !scope.isOpen; + this.toggle = function(open) { + scope.isOpen = arguments.length ? !!open : !scope.isOpen; + if (angular.isFunction(setIsOpen)) { + setIsOpen(scope, scope.isOpen); + } + + return scope.isOpen; }; // Allow other directives to watch status @@ -1678,62 +3157,193 @@ angular.module('ui.bootstrap.dropdown', []) return scope.isOpen; }; + scope.getToggleElement = function() { + return self.toggleElement; + }; + + scope.getAutoClose = function() { + return $attrs.autoClose || 'always'; //or 'outsideClick' or 'disabled' + }; + + scope.getElement = function() { + return $element; + }; + + scope.isKeynavEnabled = function() { + return keynavEnabled; + }; + + scope.focusDropdownEntry = function(keyCode) { + var elems = self.dropdownMenu ? //If append to body is used. + angular.element(self.dropdownMenu).find('a') : + $element.find('ul').eq(0).find('a'); + + switch (keyCode) { + case 40: { + if (!angular.isNumber(self.selectedOption)) { + self.selectedOption = 0; + } else { + self.selectedOption = self.selectedOption === elems.length - 1 ? + self.selectedOption : + self.selectedOption + 1; + } + break; + } + case 38: { + if (!angular.isNumber(self.selectedOption)) { + self.selectedOption = elems.length - 1; + } else { + self.selectedOption = self.selectedOption === 0 ? + 0 : self.selectedOption - 1; + } + break; + } + } + elems[self.selectedOption].focus(); + }; + + scope.getDropdownElement = function() { + return self.dropdownMenu; + }; + scope.focusToggleElement = function() { - if ( self.toggleElement ) { + if (self.toggleElement) { self.toggleElement[0].focus(); } }; - scope.$watch('isOpen', function( isOpen, wasOpen ) { - $animate[isOpen ? 'addClass' : 'removeClass'](self.$element, openClass); + scope.$watch('isOpen', function(isOpen, wasOpen) { + if (appendTo && self.dropdownMenu) { + var pos = $position.positionElements($element, self.dropdownMenu, 'bottom-left', true), + css, + rightalign; + + css = { + top: pos.top + 'px', + display: isOpen ? 'block' : 'none' + }; + + rightalign = self.dropdownMenu.hasClass('dropdown-menu-right'); + if (!rightalign) { + css.left = pos.left + 'px'; + css.right = 'auto'; + } else { + css.left = 'auto'; + css.right = window.innerWidth - + (pos.left + $element.prop('offsetWidth')) + 'px'; + } + + // Need to adjust our positioning to be relative to the appendTo container + // if it's not the body element + if (!appendToBody) { + var appendOffset = $position.offset(appendTo); + + css.top = pos.top - appendOffset.top + 'px'; + + if (!rightalign) { + css.left = pos.left - appendOffset.left + 'px'; + } else { + css.right = window.innerWidth - + (pos.left - appendOffset.left + $element.prop('offsetWidth')) + 'px'; + } + } + + self.dropdownMenu.css(css); + } + + var openContainer = appendTo ? appendTo : $element; + var hasOpenClass = openContainer.hasClass(appendTo ? appendToOpenClass : openClass); + + if (hasOpenClass === !isOpen) { + $animate[isOpen ? 'addClass' : 'removeClass'](openContainer, appendTo ? appendToOpenClass : openClass).then(function() { + if (angular.isDefined(isOpen) && isOpen !== wasOpen) { + toggleInvoker($scope, { open: !!isOpen }); + } + }); + } + + if (isOpen) { + if (self.dropdownMenuTemplateUrl) { + $templateRequest(self.dropdownMenuTemplateUrl).then(function(tplContent) { + templateScope = scope.$new(); + $compile(tplContent.trim())(templateScope, function(dropdownElement) { + var newEl = dropdownElement; + self.dropdownMenu.replaceWith(newEl); + self.dropdownMenu = newEl; + }); + }); + } - if ( isOpen ) { scope.focusToggleElement(); - dropdownService.open( scope ); + uibDropdownService.open(scope, $element); } else { - dropdownService.close( scope ); + if (self.dropdownMenuTemplateUrl) { + if (templateScope) { + templateScope.$destroy(); + } + var newEl = angular.element(''); + self.dropdownMenu.replaceWith(newEl); + self.dropdownMenu = newEl; + } + + uibDropdownService.close(scope, $element); + self.selectedOption = null; } - setIsOpen($scope, isOpen); - if (angular.isDefined(isOpen) && isOpen !== wasOpen) { - toggleInvoker($scope, { open: !!isOpen }); + if (angular.isFunction(setIsOpen)) { + setIsOpen($scope, isOpen); } }); - - $scope.$on('$locationChangeSuccess', function() { - scope.isOpen = false; - }); - - $scope.$on('$destroy', function() { - scope.$destroy(); - }); }]) -.directive('dropdown', function() { +.directive('uibDropdown', function() { return { - restrict: 'CA', - controller: 'DropdownController', + controller: 'UibDropdownController', link: function(scope, element, attrs, dropdownCtrl) { - dropdownCtrl.init( element ); + dropdownCtrl.init(); } }; }) -.directive('dropdownToggle', function() { +.directive('uibDropdownMenu', function() { return { - restrict: 'CA', - require: '?^dropdown', + restrict: 'A', + require: '?^uibDropdown', link: function(scope, element, attrs, dropdownCtrl) { - if ( !dropdownCtrl ) { + if (!dropdownCtrl || angular.isDefined(attrs.dropdownNested)) { return; } + element.addClass('dropdown-menu'); + + var tplUrl = attrs.templateUrl; + if (tplUrl) { + dropdownCtrl.dropdownMenuTemplateUrl = tplUrl; + } + + if (!dropdownCtrl.dropdownMenu) { + dropdownCtrl.dropdownMenu = element; + } + } + }; +}) + +.directive('uibDropdownToggle', function() { + return { + require: '?^uibDropdown', + link: function(scope, element, attrs, dropdownCtrl) { + if (!dropdownCtrl) { + return; + } + + element.addClass('dropdown-toggle'); + dropdownCtrl.toggleElement = element; var toggleDropdown = function(event) { event.preventDefault(); - if ( !element.hasClass('disabled') && !attrs.disabled ) { + if (!element.hasClass('disabled') && !attrs.disabled) { scope.$apply(function() { dropdownCtrl.toggle(); }); @@ -1744,7 +3354,7 @@ angular.module('ui.bootstrap.dropdown', []) // WAI-ARIA element.attr({ 'aria-haspopup': true, 'aria-expanded': false }); - scope.$watch(dropdownCtrl.isOpen, function( isOpen ) { + scope.$watch(dropdownCtrl.isOpen, function(isOpen) { element.attr('aria-expanded', !!isOpen); }); @@ -1755,27 +3365,26 @@ angular.module('ui.bootstrap.dropdown', []) }; }); -angular.module('ui.bootstrap.modal', ['ui.bootstrap.transition']) - +angular.module('ui.bootstrap.stackedMap', []) /** * A helper, internal data structure that acts as a map but also allows getting / removing * elements in the LIFO order */ - .factory('$$stackedMap', function () { + .factory('$$stackedMap', function() { return { - createNew: function () { + createNew: function() { var stack = []; return { - add: function (key, value) { + add: function(key, value) { stack.push({ key: key, value: value }); }, - get: function (key) { + get: function(key) { for (var i = 0; i < stack.length; i++) { - if (key == stack[i].key) { + if (key === stack[i].key) { return stack[i]; } } @@ -1787,93 +3396,302 @@ angular.module('ui.bootstrap.modal', ['ui.bootstrap.transition']) } return keys; }, - top: function () { + top: function() { return stack[stack.length - 1]; }, - remove: function (key) { + remove: function(key) { var idx = -1; for (var i = 0; i < stack.length; i++) { - if (key == stack[i].key) { + if (key === stack[i].key) { idx = i; break; } } return stack.splice(idx, 1)[0]; }, - removeTop: function () { + removeTop: function() { return stack.splice(stack.length - 1, 1)[0]; }, - length: function () { + length: function() { return stack.length; } }; } }; + }); +angular.module('ui.bootstrap.modal', ['ui.bootstrap.stackedMap', 'ui.bootstrap.position']) +/** + * A helper, internal data structure that stores all references attached to key + */ + .factory('$$multiMap', function() { + return { + createNew: function() { + var map = {}; + + return { + entries: function() { + return Object.keys(map).map(function(key) { + return { + key: key, + value: map[key] + }; + }); + }, + get: function(key) { + return map[key]; + }, + hasKey: function(key) { + return !!map[key]; + }, + keys: function() { + return Object.keys(map); + }, + put: function(key, value) { + if (!map[key]) { + map[key] = []; + } + + map[key].push(value); + }, + remove: function(key, value) { + var values = map[key]; + + if (!values) { + return; + } + + var idx = values.indexOf(value); + + if (idx !== -1) { + values.splice(idx, 1); + } + + if (!values.length) { + delete map[key]; + } + } + }; + } + }; + }) + +/** + * Pluggable resolve mechanism for the modal resolve resolution + * Supports UI Router's $resolve service + */ + .provider('$uibResolve', function() { + var resolve = this; + this.resolver = null; + + this.setResolver = function(resolver) { + this.resolver = resolver; + }; + + this.$get = ['$injector', '$q', function($injector, $q) { + var resolver = resolve.resolver ? $injector.get(resolve.resolver) : null; + return { + resolve: function(invocables, locals, parent, self) { + if (resolver) { + return resolver.resolve(invocables, locals, parent, self); + } + + var promises = []; + + angular.forEach(invocables, function(value) { + if (angular.isFunction(value) || angular.isArray(value)) { + promises.push($q.resolve($injector.invoke(value))); + } else if (angular.isString(value)) { + promises.push($q.resolve($injector.get(value))); + } else { + promises.push($q.resolve(value)); + } + }); + + return $q.all(promises).then(function(resolves) { + var resolveObj = {}; + var resolveIter = 0; + angular.forEach(invocables, function(value, key) { + resolveObj[key] = resolves[resolveIter++]; + }); + + return resolveObj; + }); + } + }; + }]; }) /** * A helper directive for the $modal service. It creates a backdrop element. */ - .directive('modalBackdrop', ['$timeout', function ($timeout) { + .directive('uibModalBackdrop', ['$animate', '$injector', '$uibModalStack', + function($animate, $injector, $modalStack) { return { - restrict: 'EA', replace: true, - templateUrl: 'template/modal/backdrop.html', - link: function (scope) { - - scope.animate = false; - - //trigger CSS transitions - $timeout(function () { - scope.animate = true; - }); + templateUrl: 'uib/template/modal/backdrop.html', + compile: function(tElement, tAttrs) { + tElement.addClass(tAttrs.backdropClass); + return linkFn; } }; + + function linkFn(scope, element, attrs) { + if (attrs.modalInClass) { + $animate.addClass(element, attrs.modalInClass); + + scope.$on($modalStack.NOW_CLOSING_EVENT, function(e, setIsAsync) { + var done = setIsAsync(); + if (scope.modalOptions.animation) { + $animate.removeClass(element, attrs.modalInClass).then(done); + } else { + done(); + } + }); + } + } }]) - .directive('modalWindow', ['$modalStack', '$timeout', function ($modalStack, $timeout) { + .directive('uibModalWindow', ['$uibModalStack', '$q', '$animateCss', '$document', + function($modalStack, $q, $animateCss, $document) { return { - restrict: 'EA', scope: { - index: '@', - animate: '=' + index: '@' }, replace: true, transclude: true, templateUrl: function(tElement, tAttrs) { - return tAttrs.templateUrl || 'template/modal/window.html'; + return tAttrs.templateUrl || 'uib/template/modal/window.html'; }, - link: function (scope, element, attrs) { + link: function(scope, element, attrs) { element.addClass(attrs.windowClass || ''); + element.addClass(attrs.windowTopClass || ''); scope.size = attrs.size; - $timeout(function () { - // trigger CSS transitions - scope.animate = true; - // focus a freshly-opened modal - element[0].focus(); - }); - - scope.close = function (evt) { + scope.close = function(evt) { var modal = $modalStack.getTop(); - if (modal && modal.value.backdrop && modal.value.backdrop != 'static' && (evt.target === evt.currentTarget)) { + if (modal && modal.value.backdrop && + modal.value.backdrop !== 'static' && + evt.target === evt.currentTarget) { evt.preventDefault(); evt.stopPropagation(); $modalStack.dismiss(modal.key, 'backdrop click'); } }; + + // moved from template to fix issue #2280 + element.on('click', scope.close); + + // This property is only added to the scope for the purpose of detecting when this directive is rendered. + // We can detect that by using this property in the template associated with this directive and then use + // {@link Attribute#$observe} on it. For more details please see {@link TableColumnResize}. + scope.$isRendered = true; + + // Deferred object that will be resolved when this modal is render. + var modalRenderDeferObj = $q.defer(); + // Observe function will be called on next digest cycle after compilation, ensuring that the DOM is ready. + // In order to use this way of finding whether DOM is ready, we need to observe a scope property used in modal's template. + attrs.$observe('modalRender', function(value) { + if (value === 'true') { + modalRenderDeferObj.resolve(); + } + }); + + modalRenderDeferObj.promise.then(function() { + var animationPromise = null; + + if (attrs.modalInClass) { + animationPromise = $animateCss(element, { + addClass: attrs.modalInClass + }).start(); + + scope.$on($modalStack.NOW_CLOSING_EVENT, function(e, setIsAsync) { + var done = setIsAsync(); + $animateCss(element, { + removeClass: attrs.modalInClass + }).start().then(done); + }); + } + + + $q.when(animationPromise).then(function() { + // Notify {@link $modalStack} that modal is rendered. + var modal = $modalStack.getTop(); + if (modal) { + $modalStack.modalRendered(modal.key); + } + + /** + * If something within the freshly-opened modal already has focus (perhaps via a + * directive that causes focus). then no need to try and focus anything. + */ + if (!($document[0].activeElement && element[0].contains($document[0].activeElement))) { + var inputWithAutofocus = element[0].querySelector('[autofocus]'); + /** + * Auto-focusing of a freshly-opened modal element causes any child elements + * with the autofocus attribute to lose focus. This is an issue on touch + * based devices which will show and then hide the onscreen keyboard. + * Attempts to refocus the autofocus element via JavaScript will not reopen + * the onscreen keyboard. Fixed by updated the focusing logic to only autofocus + * the modal element if the modal does not contain an autofocus element. + */ + if (inputWithAutofocus) { + inputWithAutofocus.focus(); + } else { + element[0].focus(); + } + } + }); + }); } }; }]) - .factory('$modalStack', ['$transition', '$timeout', '$document', '$compile', '$rootScope', '$$stackedMap', - function ($transition, $timeout, $document, $compile, $rootScope, $$stackedMap) { + .directive('uibModalAnimationClass', function() { + return { + compile: function(tElement, tAttrs) { + if (tAttrs.modalAnimation) { + tElement.addClass(tAttrs.uibModalAnimationClass); + } + } + }; + }) + .directive('uibModalTransclude', function() { + return { + link: function(scope, element, attrs, controller, transclude) { + transclude(scope.$parent, function(clone) { + element.empty(); + element.append(clone); + }); + } + }; + }) + + .factory('$uibModalStack', ['$animate', '$animateCss', '$document', + '$compile', '$rootScope', '$q', '$$multiMap', '$$stackedMap', '$uibPosition', + function($animate, $animateCss, $document, $compile, $rootScope, $q, $$multiMap, $$stackedMap, $uibPosition) { var OPENED_MODAL_CLASS = 'modal-open'; var backdropDomEl, backdropScope; var openedWindows = $$stackedMap.createNew(); - var $modalStack = {}; + var openedClasses = $$multiMap.createNew(); + var $modalStack = { + NOW_CLOSING_EVENT: 'modal.stack.now-closing' + }; + var topModalIndex = 0; + var previousTopOpenedModal = null; + + //Modal focus behavior + var tabableSelector = 'a[href], area[href], input:not([disabled]), ' + + 'button:not([disabled]),select:not([disabled]), textarea:not([disabled]), ' + + 'iframe, object, embed, *[tabindex], *[contenteditable=true]'; + var scrollbarPadding; + + function isVisible(element) { + return !!(element.offsetWidth || + element.offsetHeight || + element.getClientRects().length); + } function backdropIndex() { var topBackdropIndex = -1; @@ -1883,62 +3701,98 @@ angular.module('ui.bootstrap.modal', ['ui.bootstrap.transition']) topBackdropIndex = i; } } + + // If any backdrop exist, ensure that it's index is always + // right below the top modal + if (topBackdropIndex > -1 && topBackdropIndex < topModalIndex) { + topBackdropIndex = topModalIndex; + } return topBackdropIndex; } - $rootScope.$watch(backdropIndex, function(newBackdropIndex){ + $rootScope.$watch(backdropIndex, function(newBackdropIndex) { if (backdropScope) { backdropScope.index = newBackdropIndex; } }); - function removeModalWindow(modalInstance) { - - var body = $document.find('body').eq(0); + function removeModalWindow(modalInstance, elementToReceiveFocus) { var modalWindow = openedWindows.get(modalInstance).value; + var appendToElement = modalWindow.appendTo; //clean up the stack openedWindows.remove(modalInstance); + previousTopOpenedModal = openedWindows.top(); + if (previousTopOpenedModal) { + topModalIndex = parseInt(previousTopOpenedModal.value.modalDomEl.attr('index'), 10); + } - //remove window DOM element - removeAfterAnimate(modalWindow.modalDomEl, modalWindow.modalScope, 300, function() { - modalWindow.modalScope.$destroy(); - body.toggleClass(OPENED_MODAL_CLASS, openedWindows.length() > 0); - checkRemoveBackdrop(); - }); + removeAfterAnimate(modalWindow.modalDomEl, modalWindow.modalScope, function() { + var modalBodyClass = modalWindow.openedClass || OPENED_MODAL_CLASS; + openedClasses.remove(modalBodyClass, modalInstance); + var areAnyOpen = openedClasses.hasKey(modalBodyClass); + appendToElement.toggleClass(modalBodyClass, areAnyOpen); + if (!areAnyOpen && scrollbarPadding && scrollbarPadding.heightOverflow && scrollbarPadding.scrollbarWidth) { + if (scrollbarPadding.originalRight) { + appendToElement.css({paddingRight: scrollbarPadding.originalRight + 'px'}); + } else { + appendToElement.css({paddingRight: ''}); + } + scrollbarPadding = null; + } + toggleTopWindowClass(true); + }, modalWindow.closedDeferred); + checkRemoveBackdrop(); + + //move focus to specified element if available, or else to body + if (elementToReceiveFocus && elementToReceiveFocus.focus) { + elementToReceiveFocus.focus(); + } else if (appendToElement.focus) { + appendToElement.focus(); + } + } + + // Add or remove "windowTopClass" from the top window in the stack + function toggleTopWindowClass(toggleSwitch) { + var modalWindow; + + if (openedWindows.length() > 0) { + modalWindow = openedWindows.top().value; + modalWindow.modalDomEl.toggleClass(modalWindow.windowTopClass || '', toggleSwitch); + } } function checkRemoveBackdrop() { - //remove backdrop if no longer needed - if (backdropDomEl && backdropIndex() == -1) { - var backdropScopeRef = backdropScope; - removeAfterAnimate(backdropDomEl, backdropScope, 150, function () { - backdropScopeRef.$destroy(); - backdropScopeRef = null; - }); - backdropDomEl = undefined; - backdropScope = undefined; - } + //remove backdrop if no longer needed + if (backdropDomEl && backdropIndex() === -1) { + var backdropScopeRef = backdropScope; + removeAfterAnimate(backdropDomEl, backdropScope, function() { + backdropScopeRef = null; + }); + backdropDomEl = undefined; + backdropScope = undefined; + } } - function removeAfterAnimate(domEl, scope, emulateTime, done) { - // Closing animation - scope.animate = false; + function removeAfterAnimate(domEl, scope, done, closedDeferred) { + var asyncDeferred; + var asyncPromise = null; + var setIsAsync = function() { + if (!asyncDeferred) { + asyncDeferred = $q.defer(); + asyncPromise = asyncDeferred.promise; + } - var transitionEndEventName = $transition.transitionEndEventName; - if (transitionEndEventName) { - // transition out - var timeout = $timeout(afterAnimating, emulateTime); + return function asyncDone() { + asyncDeferred.resolve(); + }; + }; + scope.$broadcast($modalStack.NOW_CLOSING_EVENT, setIsAsync); - domEl.bind(transitionEndEventName, function () { - $timeout.cancel(timeout); - afterAnimating(); - scope.$apply(); - }); - } else { - // Ensure this call is async - $timeout(afterAnimating, 0); - } + // Note that it's intentional that asyncPromise might be null. + // That's when setIsAsync has not been called during the + // NOW_CLOSING_EVENT broadcast. + return $q.when(asyncPromise).then(afterAnimating); function afterAnimating() { if (afterAnimating.done) { @@ -1946,141 +3800,284 @@ angular.module('ui.bootstrap.modal', ['ui.bootstrap.transition']) } afterAnimating.done = true; - domEl.remove(); + $animate.leave(domEl).then(function() { + domEl.remove(); + if (closedDeferred) { + closedDeferred.resolve(); + } + }); + + scope.$destroy(); if (done) { done(); } } } - $document.bind('keydown', function (evt) { - var modal; + $document.on('keydown', keydownListener); - if (evt.which === 27) { - modal = openedWindows.top(); - if (modal && modal.value.keyboard) { - evt.preventDefault(); - $rootScope.$apply(function () { - $modalStack.dismiss(modal.key, 'escape key press'); - }); - } - } + $rootScope.$on('$destroy', function() { + $document.off('keydown', keydownListener); }); - $modalStack.open = function (modalInstance, modal) { + function keydownListener(evt) { + if (evt.isDefaultPrevented()) { + return evt; + } + + var modal = openedWindows.top(); + if (modal) { + switch (evt.which) { + case 27: { + if (modal.value.keyboard) { + evt.preventDefault(); + $rootScope.$apply(function() { + $modalStack.dismiss(modal.key, 'escape key press'); + }); + } + break; + } + case 9: { + var list = $modalStack.loadFocusElementList(modal); + var focusChanged = false; + if (evt.shiftKey) { + if ($modalStack.isFocusInFirstItem(evt, list) || $modalStack.isModalFocused(evt, modal)) { + focusChanged = $modalStack.focusLastFocusableElement(list); + } + } else { + if ($modalStack.isFocusInLastItem(evt, list)) { + focusChanged = $modalStack.focusFirstFocusableElement(list); + } + } + + if (focusChanged) { + evt.preventDefault(); + evt.stopPropagation(); + } + + break; + } + } + } + } + + $modalStack.open = function(modalInstance, modal) { + var modalOpener = $document[0].activeElement, + modalBodyClass = modal.openedClass || OPENED_MODAL_CLASS; + + toggleTopWindowClass(false); + + // Store the current top first, to determine what index we ought to use + // for the current top modal + previousTopOpenedModal = openedWindows.top(); openedWindows.add(modalInstance, { deferred: modal.deferred, + renderDeferred: modal.renderDeferred, + closedDeferred: modal.closedDeferred, modalScope: modal.scope, backdrop: modal.backdrop, - keyboard: modal.keyboard + keyboard: modal.keyboard, + openedClass: modal.openedClass, + windowTopClass: modal.windowTopClass, + animation: modal.animation, + appendTo: modal.appendTo }); - var body = $document.find('body').eq(0), + openedClasses.put(modalBodyClass, modalInstance); + + var appendToElement = modal.appendTo, currBackdropIndex = backdropIndex(); + if (!appendToElement.length) { + throw new Error('appendTo element not found. Make sure that the element passed is in DOM.'); + } + if (currBackdropIndex >= 0 && !backdropDomEl) { backdropScope = $rootScope.$new(true); + backdropScope.modalOptions = modal; backdropScope.index = currBackdropIndex; - backdropDomEl = $compile('
        ')(backdropScope); - body.append(backdropDomEl); + backdropDomEl = angular.element('
        '); + backdropDomEl.attr('backdrop-class', modal.backdropClass); + if (modal.animation) { + backdropDomEl.attr('modal-animation', 'true'); + } + $compile(backdropDomEl)(backdropScope); + $animate.enter(backdropDomEl, appendToElement); + scrollbarPadding = $uibPosition.scrollbarPadding(appendToElement); + if (scrollbarPadding.heightOverflow && scrollbarPadding.scrollbarWidth) { + appendToElement.css({paddingRight: scrollbarPadding.right + 'px'}); + } } - var angularDomEl = angular.element('
        '); + // Set the top modal index based on the index of the previous top modal + topModalIndex = previousTopOpenedModal ? parseInt(previousTopOpenedModal.value.modalDomEl.attr('index'), 10) + 1 : 0; + var angularDomEl = angular.element('
        '); angularDomEl.attr({ 'template-url': modal.windowTemplateUrl, 'window-class': modal.windowClass, + 'window-top-class': modal.windowTopClass, 'size': modal.size, - 'index': openedWindows.length() - 1, + 'index': topModalIndex, 'animate': 'animate' }).html(modal.content); - - var modalDomEl = $compile(angularDomEl)(modal.scope); - openedWindows.top().value.modalDomEl = modalDomEl; - body.append(modalDomEl); - body.addClass(OPENED_MODAL_CLASS); - }; - - $modalStack.close = function (modalInstance, result) { - var modalWindow = openedWindows.get(modalInstance).value; - if (modalWindow) { - modalWindow.deferred.resolve(result); - removeModalWindow(modalInstance); + if (modal.animation) { + angularDomEl.attr('modal-animation', 'true'); } + + appendToElement.addClass(modalBodyClass); + $animate.enter($compile(angularDomEl)(modal.scope), appendToElement); + + openedWindows.top().value.modalDomEl = angularDomEl; + openedWindows.top().value.modalOpener = modalOpener; }; - $modalStack.dismiss = function (modalInstance, reason) { - var modalWindow = openedWindows.get(modalInstance).value; - if (modalWindow) { - modalWindow.deferred.reject(reason); - removeModalWindow(modalInstance); + function broadcastClosing(modalWindow, resultOrReason, closing) { + return !modalWindow.value.modalScope.$broadcast('modal.closing', resultOrReason, closing).defaultPrevented; + } + + $modalStack.close = function(modalInstance, result) { + var modalWindow = openedWindows.get(modalInstance); + if (modalWindow && broadcastClosing(modalWindow, result, true)) { + modalWindow.value.modalScope.$$uibDestructionScheduled = true; + modalWindow.value.deferred.resolve(result); + removeModalWindow(modalInstance, modalWindow.value.modalOpener); + return true; } + return !modalWindow; }; - $modalStack.dismissAll = function (reason) { + $modalStack.dismiss = function(modalInstance, reason) { + var modalWindow = openedWindows.get(modalInstance); + if (modalWindow && broadcastClosing(modalWindow, reason, false)) { + modalWindow.value.modalScope.$$uibDestructionScheduled = true; + modalWindow.value.deferred.reject(reason); + removeModalWindow(modalInstance, modalWindow.value.modalOpener); + return true; + } + return !modalWindow; + }; + + $modalStack.dismissAll = function(reason) { var topModal = this.getTop(); - while (topModal) { - this.dismiss(topModal.key, reason); + while (topModal && this.dismiss(topModal.key, reason)) { topModal = this.getTop(); } }; - $modalStack.getTop = function () { + $modalStack.getTop = function() { return openedWindows.top(); }; + $modalStack.modalRendered = function(modalInstance) { + var modalWindow = openedWindows.get(modalInstance); + if (modalWindow) { + modalWindow.value.renderDeferred.resolve(); + } + }; + + $modalStack.focusFirstFocusableElement = function(list) { + if (list.length > 0) { + list[0].focus(); + return true; + } + return false; + }; + + $modalStack.focusLastFocusableElement = function(list) { + if (list.length > 0) { + list[list.length - 1].focus(); + return true; + } + return false; + }; + + $modalStack.isModalFocused = function(evt, modalWindow) { + if (evt && modalWindow) { + var modalDomEl = modalWindow.value.modalDomEl; + if (modalDomEl && modalDomEl.length) { + return (evt.target || evt.srcElement) === modalDomEl[0]; + } + } + return false; + }; + + $modalStack.isFocusInFirstItem = function(evt, list) { + if (list.length > 0) { + return (evt.target || evt.srcElement) === list[0]; + } + return false; + }; + + $modalStack.isFocusInLastItem = function(evt, list) { + if (list.length > 0) { + return (evt.target || evt.srcElement) === list[list.length - 1]; + } + return false; + }; + + $modalStack.loadFocusElementList = function(modalWindow) { + if (modalWindow) { + var modalDomE1 = modalWindow.value.modalDomEl; + if (modalDomE1 && modalDomE1.length) { + var elements = modalDomE1[0].querySelectorAll(tabableSelector); + return elements ? + Array.prototype.filter.call(elements, function(element) { + return isVisible(element); + }) : elements; + } + } + }; + return $modalStack; }]) - .provider('$modal', function () { - + .provider('$uibModal', function() { var $modalProvider = { options: { - backdrop: true, //can be also false or 'static' + animation: true, + backdrop: true, //can also be false or 'static' keyboard: true }, - $get: ['$injector', '$rootScope', '$q', '$http', '$templateCache', '$controller', '$modalStack', - function ($injector, $rootScope, $q, $http, $templateCache, $controller, $modalStack) { - + $get: ['$rootScope', '$q', '$document', '$templateRequest', '$controller', '$uibResolve', '$uibModalStack', + function ($rootScope, $q, $document, $templateRequest, $controller, $uibResolve, $modalStack) { var $modal = {}; function getTemplatePromise(options) { return options.template ? $q.when(options.template) : - $http.get(options.templateUrl, {cache: $templateCache}).then(function (result) { - return result.data; - }); + $templateRequest(angular.isFunction(options.templateUrl) ? + options.templateUrl() : options.templateUrl); } - function getResolvePromises(resolves) { - var promisesArr = []; - angular.forEach(resolves, function (value, key) { - if (angular.isFunction(value) || angular.isArray(value)) { - promisesArr.push($q.when($injector.invoke(value))); - } - }); - return promisesArr; - } - - $modal.open = function (modalOptions) { + var promiseChain = null; + $modal.getPromiseChain = function() { + return promiseChain; + }; + $modal.open = function(modalOptions) { var modalResultDeferred = $q.defer(); var modalOpenedDeferred = $q.defer(); + var modalClosedDeferred = $q.defer(); + var modalRenderDeferred = $q.defer(); //prepare an instance of a modal to be injected into controllers and returned to a caller var modalInstance = { result: modalResultDeferred.promise, opened: modalOpenedDeferred.promise, + closed: modalClosedDeferred.promise, + rendered: modalRenderDeferred.promise, close: function (result) { - $modalStack.close(modalInstance, result); + return $modalStack.close(modalInstance, result); }, dismiss: function (reason) { - $modalStack.dismiss(modalInstance, reason); + return $modalStack.dismiss(modalInstance, reason); } }; //merge and clean up options modalOptions = angular.extend({}, $modalProvider.options, modalOptions); modalOptions.resolve = modalOptions.resolve || {}; + modalOptions.appendTo = modalOptions.appendTo || $document.find('body').eq(0); //verify options if (!modalOptions.template && !modalOptions.templateUrl) { @@ -2088,261 +4085,379 @@ angular.module('ui.bootstrap.modal', ['ui.bootstrap.transition']) } var templateAndResolvePromise = - $q.all([getTemplatePromise(modalOptions)].concat(getResolvePromises(modalOptions.resolve))); + $q.all([getTemplatePromise(modalOptions), $uibResolve.resolve(modalOptions.resolve, {}, null, null)]); + function resolveWithTemplate() { + return templateAndResolvePromise; + } - templateAndResolvePromise.then(function resolveSuccess(tplAndVars) { + // Wait for the resolution of the existing promise chain. + // Then switch to our own combined promise dependency (regardless of how the previous modal fared). + // Then add to $modalStack and resolve opened. + // Finally clean up the chain variable if no subsequent modal has overwritten it. + var samePromise; + samePromise = promiseChain = $q.all([promiseChain]) + .then(resolveWithTemplate, resolveWithTemplate) + .then(function resolveSuccess(tplAndVars) { + var providedScope = modalOptions.scope || $rootScope; - var modalScope = (modalOptions.scope || $rootScope).$new(); - modalScope.$close = modalInstance.close; - modalScope.$dismiss = modalInstance.dismiss; + var modalScope = providedScope.$new(); + modalScope.$close = modalInstance.close; + modalScope.$dismiss = modalInstance.dismiss; - var ctrlInstance, ctrlLocals = {}; - var resolveIter = 1; - - //controllers - if (modalOptions.controller) { - ctrlLocals.$scope = modalScope; - ctrlLocals.$modalInstance = modalInstance; - angular.forEach(modalOptions.resolve, function (value, key) { - ctrlLocals[key] = tplAndVars[resolveIter++]; + modalScope.$on('$destroy', function() { + if (!modalScope.$$uibDestructionScheduled) { + modalScope.$dismiss('$uibUnscheduledDestruction'); + } }); - ctrlInstance = $controller(modalOptions.controller, ctrlLocals); - } + var ctrlInstance, ctrlInstantiate, ctrlLocals = {}; - $modalStack.open(modalInstance, { - scope: modalScope, - deferred: modalResultDeferred, - content: tplAndVars[0], - backdrop: modalOptions.backdrop, - keyboard: modalOptions.keyboard, - windowClass: modalOptions.windowClass, - windowTemplateUrl: modalOptions.windowTemplateUrl, - size: modalOptions.size - }); + //controllers + if (modalOptions.controller) { + ctrlLocals.$scope = modalScope; + ctrlLocals.$uibModalInstance = modalInstance; + angular.forEach(tplAndVars[1], function(value, key) { + ctrlLocals[key] = value; + }); + + // the third param will make the controller instantiate later,private api + // @see https://github.com/angular/angular.js/blob/master/src/ng/controller.js#L126 + ctrlInstantiate = $controller(modalOptions.controller, ctrlLocals, true); + if (modalOptions.controllerAs) { + ctrlInstance = ctrlInstantiate.instance; + + if (modalOptions.bindToController) { + ctrlInstance.$close = modalScope.$close; + ctrlInstance.$dismiss = modalScope.$dismiss; + angular.extend(ctrlInstance, providedScope); + } + + ctrlInstance = ctrlInstantiate(); + + modalScope[modalOptions.controllerAs] = ctrlInstance; + } else { + ctrlInstance = ctrlInstantiate(); + } + + if (angular.isFunction(ctrlInstance.$onInit)) { + ctrlInstance.$onInit(); + } + } + + $modalStack.open(modalInstance, { + scope: modalScope, + deferred: modalResultDeferred, + renderDeferred: modalRenderDeferred, + closedDeferred: modalClosedDeferred, + content: tplAndVars[0], + animation: modalOptions.animation, + backdrop: modalOptions.backdrop, + keyboard: modalOptions.keyboard, + backdropClass: modalOptions.backdropClass, + windowTopClass: modalOptions.windowTopClass, + windowClass: modalOptions.windowClass, + windowTemplateUrl: modalOptions.windowTemplateUrl, + size: modalOptions.size, + openedClass: modalOptions.openedClass, + appendTo: modalOptions.appendTo + }); + modalOpenedDeferred.resolve(true); }, function resolveError(reason) { + modalOpenedDeferred.reject(reason); modalResultDeferred.reject(reason); - }); - - templateAndResolvePromise.then(function () { - modalOpenedDeferred.resolve(true); - }, function () { - modalOpenedDeferred.reject(false); + })['finally'](function() { + if (promiseChain === samePromise) { + promiseChain = null; + } }); return modalInstance; }; return $modal; - }] + } + ] }; return $modalProvider; }); -angular.module('ui.bootstrap.pagination', []) - -.controller('PaginationController', ['$scope', '$attrs', '$parse', function ($scope, $attrs, $parse) { - var self = this, - ngModelCtrl = { $setViewValue: angular.noop }, // nullModelCtrl - setNumPages = $attrs.numPages ? $parse($attrs.numPages).assign : angular.noop; - - this.init = function(ngModelCtrl_, config) { - ngModelCtrl = ngModelCtrl_; - this.config = config; - - ngModelCtrl.$render = function() { - self.render(); - }; - - if ($attrs.itemsPerPage) { - $scope.$parent.$watch($parse($attrs.itemsPerPage), function(value) { - self.itemsPerPage = parseInt(value, 10); - $scope.totalPages = self.calculateTotalPages(); - }); - } else { - this.itemsPerPage = config.itemsPerPage; - } - }; - - this.calculateTotalPages = function() { - var totalPages = this.itemsPerPage < 1 ? 1 : Math.ceil($scope.totalItems / this.itemsPerPage); - return Math.max(totalPages || 0, 1); - }; - - this.render = function() { - $scope.page = parseInt(ngModelCtrl.$viewValue, 10) || 1; - }; - - $scope.selectPage = function(page) { - if ( $scope.page !== page && page > 0 && page <= $scope.totalPages) { - ngModelCtrl.$setViewValue(page); - ngModelCtrl.$render(); - } - }; - - $scope.getText = function( key ) { - return $scope[key + 'Text'] || self.config[key + 'Text']; - }; - $scope.noPrevious = function() { - return $scope.page === 1; - }; - $scope.noNext = function() { - return $scope.page === $scope.totalPages; - }; - - $scope.$watch('totalItems', function() { - $scope.totalPages = self.calculateTotalPages(); - }); - - $scope.$watch('totalPages', function(value) { - setNumPages($scope.$parent, value); // Readonly variable - - if ( $scope.page > value ) { - $scope.selectPage(value); - } else { - ngModelCtrl.$render(); - } - }); -}]) - -.constant('paginationConfig', { - itemsPerPage: 10, - boundaryLinks: false, - directionLinks: true, - firstText: 'First', - previousText: 'Previous', - nextText: 'Next', - lastText: 'Last', - rotate: true -}) - -.directive('pagination', ['$parse', 'paginationConfig', function($parse, paginationConfig) { +angular.module('ui.bootstrap.paging', []) +/** + * Helper internal service for generating common controller code between the + * pager and pagination components + */ +.factory('uibPaging', ['$parse', function($parse) { return { - restrict: 'EA', - scope: { - totalItems: '=', - firstText: '@', - previousText: '@', - nextText: '@', - lastText: '@' - }, - require: ['pagination', '?ngModel'], - controller: 'PaginationController', - templateUrl: 'template/pagination/pagination.html', - replace: true, - link: function(scope, element, attrs, ctrls) { - var paginationCtrl = ctrls[0], ngModelCtrl = ctrls[1]; + create: function(ctrl, $scope, $attrs) { + ctrl.setNumPages = $attrs.numPages ? $parse($attrs.numPages).assign : angular.noop; + ctrl.ngModelCtrl = { $setViewValue: angular.noop }; // nullModelCtrl + ctrl._watchers = []; - if (!ngModelCtrl) { - return; // do nothing if no ng-model - } + ctrl.init = function(ngModelCtrl, config) { + ctrl.ngModelCtrl = ngModelCtrl; + ctrl.config = config; - // Setup configuration parameters - var maxSize = angular.isDefined(attrs.maxSize) ? scope.$parent.$eval(attrs.maxSize) : paginationConfig.maxSize, - rotate = angular.isDefined(attrs.rotate) ? scope.$parent.$eval(attrs.rotate) : paginationConfig.rotate; - scope.boundaryLinks = angular.isDefined(attrs.boundaryLinks) ? scope.$parent.$eval(attrs.boundaryLinks) : paginationConfig.boundaryLinks; - scope.directionLinks = angular.isDefined(attrs.directionLinks) ? scope.$parent.$eval(attrs.directionLinks) : paginationConfig.directionLinks; - - paginationCtrl.init(ngModelCtrl, paginationConfig); - - if (attrs.maxSize) { - scope.$parent.$watch($parse(attrs.maxSize), function(value) { - maxSize = parseInt(value, 10); - paginationCtrl.render(); - }); - } - - // Create page object used in template - function makePage(number, text, isActive) { - return { - number: number, - text: text, - active: isActive + ngModelCtrl.$render = function() { + ctrl.render(); }; - } - function getPages(currentPage, totalPages) { - var pages = []; - - // Default page limits - var startPage = 1, endPage = totalPages; - var isMaxSized = ( angular.isDefined(maxSize) && maxSize < totalPages ); - - // recompute if maxSize - if ( isMaxSized ) { - if ( rotate ) { - // Current page is displayed in the middle of the visible ones - startPage = Math.max(currentPage - Math.floor(maxSize/2), 1); - endPage = startPage + maxSize - 1; - - // Adjust if limit is exceeded - if (endPage > totalPages) { - endPage = totalPages; - startPage = endPage - maxSize + 1; - } - } else { - // Visible pages are paginated with maxSize - startPage = ((Math.ceil(currentPage / maxSize) - 1) * maxSize) + 1; - - // Adjust last page if limit is exceeded - endPage = Math.min(startPage + maxSize - 1, totalPages); - } + if ($attrs.itemsPerPage) { + ctrl._watchers.push($scope.$parent.$watch($attrs.itemsPerPage, function(value) { + ctrl.itemsPerPage = parseInt(value, 10); + $scope.totalPages = ctrl.calculateTotalPages(); + ctrl.updatePage(); + })); + } else { + ctrl.itemsPerPage = config.itemsPerPage; } - // Add page number links - for (var number = startPage; number <= endPage; number++) { - var page = makePage(number, number, number === currentPage); - pages.push(page); + $scope.$watch('totalItems', function(newTotal, oldTotal) { + if (angular.isDefined(newTotal) || newTotal !== oldTotal) { + $scope.totalPages = ctrl.calculateTotalPages(); + ctrl.updatePage(); + } + }); + }; + + ctrl.calculateTotalPages = function() { + var totalPages = ctrl.itemsPerPage < 1 ? 1 : Math.ceil($scope.totalItems / ctrl.itemsPerPage); + return Math.max(totalPages || 0, 1); + }; + + ctrl.render = function() { + $scope.page = parseInt(ctrl.ngModelCtrl.$viewValue, 10) || 1; + }; + + $scope.selectPage = function(page, evt) { + if (evt) { + evt.preventDefault(); } - // Add links to move between page sets - if ( isMaxSized && ! rotate ) { - if ( startPage > 1 ) { - var previousPageSet = makePage(startPage - 1, '...', false); - pages.unshift(previousPageSet); + var clickAllowed = !$scope.ngDisabled || !evt; + if (clickAllowed && $scope.page !== page && page > 0 && page <= $scope.totalPages) { + if (evt && evt.target) { + evt.target.blur(); } - - if ( endPage < totalPages ) { - var nextPageSet = makePage(endPage + 1, '...', false); - pages.push(nextPageSet); - } - } - - return pages; - } - - var originalRender = paginationCtrl.render; - paginationCtrl.render = function() { - originalRender(); - if (scope.page > 0 && scope.page <= scope.totalPages) { - scope.pages = getPages(scope.page, scope.totalPages); + ctrl.ngModelCtrl.$setViewValue(page); + ctrl.ngModelCtrl.$render(); } }; + + $scope.getText = function(key) { + return $scope[key + 'Text'] || ctrl.config[key + 'Text']; + }; + + $scope.noPrevious = function() { + return $scope.page === 1; + }; + + $scope.noNext = function() { + return $scope.page === $scope.totalPages; + }; + + ctrl.updatePage = function() { + ctrl.setNumPages($scope.$parent, $scope.totalPages); // Readonly variable + + if ($scope.page > $scope.totalPages) { + $scope.selectPage($scope.totalPages); + } else { + ctrl.ngModelCtrl.$render(); + } + }; + + $scope.$on('$destroy', function() { + while (ctrl._watchers.length) { + ctrl._watchers.shift()(); + } + }); } }; +}]); + +angular.module('ui.bootstrap.pager', ['ui.bootstrap.paging']) + +.controller('UibPagerController', ['$scope', '$attrs', 'uibPaging', 'uibPagerConfig', function($scope, $attrs, uibPaging, uibPagerConfig) { + $scope.align = angular.isDefined($attrs.align) ? $scope.$parent.$eval($attrs.align) : uibPagerConfig.align; + + uibPaging.create(this, $scope, $attrs); }]) -.constant('pagerConfig', { +.constant('uibPagerConfig', { itemsPerPage: 10, previousText: '« Previous', nextText: 'Next »', align: true }) -.directive('pager', ['pagerConfig', function(pagerConfig) { +.directive('uibPager', ['uibPagerConfig', function(uibPagerConfig) { return { - restrict: 'EA', scope: { totalItems: '=', previousText: '@', - nextText: '@' + nextText: '@', + ngDisabled: '=' + }, + require: ['uibPager', '?ngModel'], + controller: 'UibPagerController', + controllerAs: 'pager', + templateUrl: function(element, attrs) { + return attrs.templateUrl || 'uib/template/pager/pager.html'; + }, + replace: true, + link: function(scope, element, attrs, ctrls) { + var paginationCtrl = ctrls[0], ngModelCtrl = ctrls[1]; + + if (!ngModelCtrl) { + return; // do nothing if no ng-model + } + + paginationCtrl.init(ngModelCtrl, uibPagerConfig); + } + }; +}]); + +angular.module('ui.bootstrap.pagination', ['ui.bootstrap.paging']) +.controller('UibPaginationController', ['$scope', '$attrs', '$parse', 'uibPaging', 'uibPaginationConfig', function($scope, $attrs, $parse, uibPaging, uibPaginationConfig) { + var ctrl = this; + // Setup configuration parameters + var maxSize = angular.isDefined($attrs.maxSize) ? $scope.$parent.$eval($attrs.maxSize) : uibPaginationConfig.maxSize, + rotate = angular.isDefined($attrs.rotate) ? $scope.$parent.$eval($attrs.rotate) : uibPaginationConfig.rotate, + forceEllipses = angular.isDefined($attrs.forceEllipses) ? $scope.$parent.$eval($attrs.forceEllipses) : uibPaginationConfig.forceEllipses, + boundaryLinkNumbers = angular.isDefined($attrs.boundaryLinkNumbers) ? $scope.$parent.$eval($attrs.boundaryLinkNumbers) : uibPaginationConfig.boundaryLinkNumbers, + pageLabel = angular.isDefined($attrs.pageLabel) ? function(idx) { return $scope.$parent.$eval($attrs.pageLabel, {$page: idx}); } : angular.identity; + $scope.boundaryLinks = angular.isDefined($attrs.boundaryLinks) ? $scope.$parent.$eval($attrs.boundaryLinks) : uibPaginationConfig.boundaryLinks; + $scope.directionLinks = angular.isDefined($attrs.directionLinks) ? $scope.$parent.$eval($attrs.directionLinks) : uibPaginationConfig.directionLinks; + + uibPaging.create(this, $scope, $attrs); + + if ($attrs.maxSize) { + ctrl._watchers.push($scope.$parent.$watch($parse($attrs.maxSize), function(value) { + maxSize = parseInt(value, 10); + ctrl.render(); + })); + } + + // Create page object used in template + function makePage(number, text, isActive) { + return { + number: number, + text: text, + active: isActive + }; + } + + function getPages(currentPage, totalPages) { + var pages = []; + + // Default page limits + var startPage = 1, endPage = totalPages; + var isMaxSized = angular.isDefined(maxSize) && maxSize < totalPages; + + // recompute if maxSize + if (isMaxSized) { + if (rotate) { + // Current page is displayed in the middle of the visible ones + startPage = Math.max(currentPage - Math.floor(maxSize / 2), 1); + endPage = startPage + maxSize - 1; + + // Adjust if limit is exceeded + if (endPage > totalPages) { + endPage = totalPages; + startPage = endPage - maxSize + 1; + } + } else { + // Visible pages are paginated with maxSize + startPage = (Math.ceil(currentPage / maxSize) - 1) * maxSize + 1; + + // Adjust last page if limit is exceeded + endPage = Math.min(startPage + maxSize - 1, totalPages); + } + } + + // Add page number links + for (var number = startPage; number <= endPage; number++) { + var page = makePage(number, pageLabel(number), number === currentPage); + pages.push(page); + } + + // Add links to move between page sets + if (isMaxSized && maxSize > 0 && (!rotate || forceEllipses || boundaryLinkNumbers)) { + if (startPage > 1) { + if (!boundaryLinkNumbers || startPage > 3) { //need ellipsis for all options unless range is too close to beginning + var previousPageSet = makePage(startPage - 1, '...', false); + pages.unshift(previousPageSet); + } + if (boundaryLinkNumbers) { + if (startPage === 3) { //need to replace ellipsis when the buttons would be sequential + var secondPageLink = makePage(2, '2', false); + pages.unshift(secondPageLink); + } + //add the first page + var firstPageLink = makePage(1, '1', false); + pages.unshift(firstPageLink); + } + } + + if (endPage < totalPages) { + if (!boundaryLinkNumbers || endPage < totalPages - 2) { //need ellipsis for all options unless range is too close to end + var nextPageSet = makePage(endPage + 1, '...', false); + pages.push(nextPageSet); + } + if (boundaryLinkNumbers) { + if (endPage === totalPages - 2) { //need to replace ellipsis when the buttons would be sequential + var secondToLastPageLink = makePage(totalPages - 1, totalPages - 1, false); + pages.push(secondToLastPageLink); + } + //add the last page + var lastPageLink = makePage(totalPages, totalPages, false); + pages.push(lastPageLink); + } + } + } + return pages; + } + + var originalRender = this.render; + this.render = function() { + originalRender(); + if ($scope.page > 0 && $scope.page <= $scope.totalPages) { + $scope.pages = getPages($scope.page, $scope.totalPages); + } + }; +}]) + +.constant('uibPaginationConfig', { + itemsPerPage: 10, + boundaryLinks: false, + boundaryLinkNumbers: false, + directionLinks: true, + firstText: 'First', + previousText: 'Previous', + nextText: 'Next', + lastText: 'Last', + rotate: true, + forceEllipses: false +}) + +.directive('uibPagination', ['$parse', 'uibPaginationConfig', function($parse, uibPaginationConfig) { + return { + scope: { + totalItems: '=', + firstText: '@', + previousText: '@', + nextText: '@', + lastText: '@', + ngDisabled:'=' + }, + require: ['uibPagination', '?ngModel'], + controller: 'UibPaginationController', + controllerAs: 'pagination', + templateUrl: function(element, attrs) { + return attrs.templateUrl || 'uib/template/pagination/pagination.html'; }, - require: ['pager', '?ngModel'], - controller: 'PaginationController', - templateUrl: 'template/pagination/pager.html', replace: true, link: function(scope, element, attrs, ctrls) { var paginationCtrl = ctrls[0], ngModelCtrl = ctrls[1]; @@ -2351,8 +4466,7 @@ angular.module('ui.bootstrap.pagination', []) return; // do nothing if no ng-model } - scope.align = angular.isDefined(attrs.align) ? scope.$parent.$eval(attrs.align) : pagerConfig.align; - paginationCtrl.init(ngModelCtrl, pagerConfig); + paginationCtrl.init(ngModelCtrl, uibPaginationConfig); } }; }]); @@ -2362,25 +4476,30 @@ angular.module('ui.bootstrap.pagination', []) * function, placement as a function, inside, support for more triggers than * just mouse enter/leave, html tooltips, and selector delegation. */ -angular.module( 'ui.bootstrap.tooltip', [ 'ui.bootstrap.position', 'ui.bootstrap.bindHtml' ] ) +angular.module('ui.bootstrap.tooltip', ['ui.bootstrap.position', 'ui.bootstrap.stackedMap']) /** * The $tooltip service creates tooltip- and popover-like directives as well as * houses global options for them. */ -.provider( '$tooltip', function () { +.provider('$uibTooltip', function() { // The default options tooltip and popover. var defaultOptions = { placement: 'top', + placementClassPrefix: '', animation: true, - popupDelay: 0 + popupDelay: 0, + popupCloseDelay: 0, + useContentExp: false }; // Default hide triggers for each show trigger var triggerMap = { 'mouseenter': 'mouseleave', 'click': 'click', - 'focus': 'blur' + 'outsideClick': 'outsideClick', + 'focus': 'blur', + 'none': '' }; // The options specified to the provider globally. @@ -2395,23 +4514,23 @@ angular.module( 'ui.bootstrap.tooltip', [ 'ui.bootstrap.position', 'ui.bootstrap * $tooltipProvider.options( { placement: 'left' } ); * }); */ - this.options = function( value ) { - angular.extend( globalOptions, value ); + this.options = function(value) { + angular.extend(globalOptions, value); }; /** * This allows you to extend the set of trigger mappings available. E.g.: * - * $tooltipProvider.setTriggers( 'openTrigger': 'closeTrigger' ); + * $tooltipProvider.setTriggers( { 'openTrigger': 'closeTrigger' } ); */ - this.setTriggers = function setTriggers ( triggers ) { - angular.extend( triggerMap, triggers ); + this.setTriggers = function setTriggers(triggers) { + angular.extend(triggerMap, triggers); }; /** - * This is a helper function for translating camel-case to snake-case. + * This is a helper function for translating camel-case to snake_case. */ - function snake_case(name){ + function snake_case(name) { var regexp = /[A-Z]/g; var separator = '-'; return name.replace(regexp, function(letter, pos) { @@ -2423,9 +4542,27 @@ angular.module( 'ui.bootstrap.tooltip', [ 'ui.bootstrap.position', 'ui.bootstrap * Returns the actual instance of the $tooltip service. * TODO support multiple triggers */ - this.$get = [ '$window', '$compile', '$timeout', '$parse', '$document', '$position', '$interpolate', function ( $window, $compile, $timeout, $parse, $document, $position, $interpolate ) { - return function $tooltip ( type, prefix, defaultTriggerShow ) { - var options = angular.extend( {}, defaultOptions, globalOptions ); + this.$get = ['$window', '$compile', '$timeout', '$document', '$uibPosition', '$interpolate', '$rootScope', '$parse', '$$stackedMap', function($window, $compile, $timeout, $document, $position, $interpolate, $rootScope, $parse, $$stackedMap) { + var openedTooltips = $$stackedMap.createNew(); + $document.on('keypress', keypressListener); + + $rootScope.$on('$destroy', function() { + $document.off('keypress', keypressListener); + }); + + function keypressListener(e) { + if (e.which === 27) { + var last = openedTooltips.top(); + if (last) { + last.value.close(); + openedTooltips.removeTop(); + last = null; + } + } + } + + return function $tooltip(ttType, prefix, defaultTriggerShow, options) { + options = angular.extend({}, defaultOptions, globalOptions, options); /** * Returns an object of show and hide triggers. @@ -2441,59 +4578,104 @@ angular.module( 'ui.bootstrap.tooltip', [ 'ui.bootstrap.position', 'ui.bootstrap * undefined; otherwise, it uses the `triggerMap` value of the show * trigger; else it will just use the show trigger. */ - function getTriggers ( trigger ) { - var show = trigger || options.trigger || defaultTriggerShow; - var hide = triggerMap[show] || show; + function getTriggers(trigger) { + var show = (trigger || options.trigger || defaultTriggerShow).split(' '); + var hide = show.map(function(trigger) { + return triggerMap[trigger] || trigger; + }); return { show: show, hide: hide }; } - var directiveName = snake_case( type ); + var directiveName = snake_case(ttType); var startSym = $interpolate.startSymbol(); var endSym = $interpolate.endSymbol(); var template = - '
        '+ + '
        ' + '
        '; return { - restrict: 'EA', - scope: true, - compile: function (tElem, tAttrs) { - var tooltipLinker = $compile( template ); + compile: function(tElem, tAttrs) { + var tooltipLinker = $compile(template); - return function link ( scope, element, attrs ) { + return function link(scope, element, attrs, tooltipCtrl) { var tooltip; + var tooltipLinkedScope; var transitionTimeout; - var popupTimeout; - var appendToBody = angular.isDefined( options.appendToBody ) ? options.appendToBody : false; - var triggers = getTriggers( undefined ); - var hasEnableExp = angular.isDefined(attrs[prefix+'Enable']); + var showTimeout; + var hideTimeout; + var positionTimeout; + var appendToBody = angular.isDefined(options.appendToBody) ? options.appendToBody : false; + var triggers = getTriggers(undefined); + var hasEnableExp = angular.isDefined(attrs[prefix + 'Enable']); + var ttScope = scope.$new(true); + var repositionScheduled = false; + var isOpenParse = angular.isDefined(attrs[prefix + 'IsOpen']) ? $parse(attrs[prefix + 'IsOpen']) : false; + var contentParse = options.useContentExp ? $parse(attrs[ttType]) : false; + var observers = []; + var lastPlacement; - var positionTooltip = function () { + var positionTooltip = function() { + // check if tooltip exists and is not empty + if (!tooltip || !tooltip.html()) { return; } - var ttPosition = $position.positionElements(element, tooltip, scope.tt_placement, appendToBody); - ttPosition.top += 'px'; - ttPosition.left += 'px'; + if (!positionTimeout) { + positionTimeout = $timeout(function() { + var ttPosition = $position.positionElements(element, tooltip, ttScope.placement, appendToBody); + tooltip.css({ top: ttPosition.top + 'px', left: ttPosition.left + 'px' }); - // Now set the calculated positioning. - tooltip.css( ttPosition ); + if (!tooltip.hasClass(ttPosition.placement.split('-')[0])) { + tooltip.removeClass(lastPlacement.split('-')[0]); + tooltip.addClass(ttPosition.placement.split('-')[0]); + } + + if (!tooltip.hasClass(options.placementClassPrefix + ttPosition.placement)) { + tooltip.removeClass(options.placementClassPrefix + lastPlacement); + tooltip.addClass(options.placementClassPrefix + ttPosition.placement); + } + + // first time through tt element will have the + // uib-position-measure class or if the placement + // has changed we need to position the arrow. + if (tooltip.hasClass('uib-position-measure')) { + $position.positionArrow(tooltip, ttPosition.placement); + tooltip.removeClass('uib-position-measure'); + } else if (lastPlacement !== ttPosition.placement) { + $position.positionArrow(tooltip, ttPosition.placement); + } + lastPlacement = ttPosition.placement; + + positionTimeout = null; + }, 0, false); + } }; + // Set up the correct scope to allow transclusion later + ttScope.origScope = scope; + // By default, the tooltip is not open. // TODO add ability to start tooltip opened - scope.tt_isOpen = false; + ttScope.isOpen = false; + openedTooltips.add(ttScope, { + close: hide + }); - function toggleTooltipBind () { - if ( ! scope.tt_isOpen ) { + function toggleTooltipBind() { + if (!ttScope.isOpen) { showTooltipBind(); } else { hideTooltipBind(); @@ -2502,174 +4684,338 @@ angular.module( 'ui.bootstrap.tooltip', [ 'ui.bootstrap.position', 'ui.bootstrap // Show the tooltip with delay if specified, otherwise show it immediately function showTooltipBind() { - if(hasEnableExp && !scope.$eval(attrs[prefix+'Enable'])) { + if (hasEnableExp && !scope.$eval(attrs[prefix + 'Enable'])) { return; } - if ( scope.tt_popupDelay ) { + + cancelHide(); + prepareTooltip(); + + if (ttScope.popupDelay) { // Do nothing if the tooltip was already scheduled to pop-up. // This happens if show is triggered multiple times before any hide is triggered. - if (!popupTimeout) { - popupTimeout = $timeout( show, scope.tt_popupDelay, false ); - popupTimeout.then(function(reposition){reposition();}); + if (!showTimeout) { + showTimeout = $timeout(show, ttScope.popupDelay, false); } } else { - show()(); + show(); } } - function hideTooltipBind () { - scope.$apply(function () { + function hideTooltipBind() { + cancelShow(); + + if (ttScope.popupCloseDelay) { + if (!hideTimeout) { + hideTimeout = $timeout(hide, ttScope.popupCloseDelay, false); + } + } else { hide(); - }); + } } // Show the tooltip popup element. function show() { - - popupTimeout = null; - - // If there is a pending remove transition, we must cancel it, lest the - // tooltip be mysteriously removed. - if ( transitionTimeout ) { - $timeout.cancel( transitionTimeout ); - transitionTimeout = null; - } + cancelShow(); + cancelHide(); // Don't show empty tooltips. - if ( ! scope.tt_content ) { + if (!ttScope.content) { return angular.noop; } createTooltip(); - // Set the initial positioning. - tooltip.css({ top: 0, left: 0, display: 'block' }); + // And show the tooltip. + ttScope.$evalAsync(function() { + ttScope.isOpen = true; + assignIsOpen(true); + positionTooltip(); + }); + } - // Now we add it to the DOM because need some info about it. But it's not - // visible yet anyway. - if ( appendToBody ) { - $document.find( 'body' ).append( tooltip ); - } else { - element.after( tooltip ); + function cancelShow() { + if (showTimeout) { + $timeout.cancel(showTimeout); + showTimeout = null; } - positionTooltip(); - - // And show the tooltip. - scope.tt_isOpen = true; - scope.$digest(); // digest required as $apply is not called - - // Return positioning function as promise callback for correct - // positioning after draw. - return positionTooltip; + if (positionTimeout) { + $timeout.cancel(positionTimeout); + positionTimeout = null; + } } // Hide the tooltip popup element. function hide() { + if (!ttScope) { + return; + } + // First things first: we don't show it anymore. - scope.tt_isOpen = false; - - //if tooltip is going to be shown after delay, we must cancel this - $timeout.cancel( popupTimeout ); - popupTimeout = null; - - // And now we remove it from the DOM. However, if we have animation, we - // need to wait for it to expire beforehand. - // FIXME: this is a placeholder for a port of the transitions library. - if ( scope.tt_animation ) { - if (!transitionTimeout) { - transitionTimeout = $timeout(removeTooltip, 500); + ttScope.$evalAsync(function() { + if (ttScope) { + ttScope.isOpen = false; + assignIsOpen(false); + // And now we remove it from the DOM. However, if we have animation, we + // need to wait for it to expire beforehand. + // FIXME: this is a placeholder for a port of the transitions library. + // The fade transition in TWBS is 150ms. + if (ttScope.animation) { + if (!transitionTimeout) { + transitionTimeout = $timeout(removeTooltip, 150, false); + } + } else { + removeTooltip(); + } } - } else { - removeTooltip(); + }); + } + + function cancelHide() { + if (hideTimeout) { + $timeout.cancel(hideTimeout); + hideTimeout = null; + } + + if (transitionTimeout) { + $timeout.cancel(transitionTimeout); + transitionTimeout = null; } } function createTooltip() { // There can only be one tooltip element per directive shown at once. if (tooltip) { - removeTooltip(); + return; } - tooltip = tooltipLinker(scope, function () {}); - // Get contents rendered into the tooltip - scope.$digest(); + tooltipLinkedScope = ttScope.$new(); + tooltip = tooltipLinker(tooltipLinkedScope, function(tooltip) { + if (appendToBody) { + $document.find('body').append(tooltip); + } else { + element.after(tooltip); + } + }); + + prepObservers(); } function removeTooltip() { - transitionTimeout = null; + cancelShow(); + cancelHide(); + unregisterObservers(); + if (tooltip) { tooltip.remove(); tooltip = null; } + if (tooltipLinkedScope) { + tooltipLinkedScope.$destroy(); + tooltipLinkedScope = null; + } } + /** + * Set the initial scope values. Once + * the tooltip is created, the observers + * will be added to keep things in sync. + */ + function prepareTooltip() { + ttScope.title = attrs[prefix + 'Title']; + if (contentParse) { + ttScope.content = contentParse(scope); + } else { + ttScope.content = attrs[ttType]; + } + + ttScope.popupClass = attrs[prefix + 'Class']; + ttScope.placement = angular.isDefined(attrs[prefix + 'Placement']) ? attrs[prefix + 'Placement'] : options.placement; + var placement = $position.parsePlacement(ttScope.placement); + lastPlacement = placement[1] ? placement[0] + '-' + placement[1] : placement[0]; + + var delay = parseInt(attrs[prefix + 'PopupDelay'], 10); + var closeDelay = parseInt(attrs[prefix + 'PopupCloseDelay'], 10); + ttScope.popupDelay = !isNaN(delay) ? delay : options.popupDelay; + ttScope.popupCloseDelay = !isNaN(closeDelay) ? closeDelay : options.popupCloseDelay; + } + + function assignIsOpen(isOpen) { + if (isOpenParse && angular.isFunction(isOpenParse.assign)) { + isOpenParse.assign(scope, isOpen); + } + } + + ttScope.contentExp = function() { + return ttScope.content; + }; + /** * Observe the relevant attributes. */ - attrs.$observe( type, function ( val ) { - scope.tt_content = val; + attrs.$observe('disabled', function(val) { + if (val) { + cancelShow(); + } - if (!val && scope.tt_isOpen ) { + if (val && ttScope.isOpen) { hide(); } }); - attrs.$observe( prefix+'Title', function ( val ) { - scope.tt_title = val; - }); + if (isOpenParse) { + scope.$watch(isOpenParse, function(val) { + if (ttScope && !val === ttScope.isOpen) { + toggleTooltipBind(); + } + }); + } - attrs.$observe( prefix+'Placement', function ( val ) { - scope.tt_placement = angular.isDefined( val ) ? val : options.placement; - }); + function prepObservers() { + observers.length = 0; - attrs.$observe( prefix+'PopupDelay', function ( val ) { - var delay = parseInt( val, 10 ); - scope.tt_popupDelay = ! isNaN(delay) ? delay : options.popupDelay; - }); + if (contentParse) { + observers.push( + scope.$watch(contentParse, function(val) { + ttScope.content = val; + if (!val && ttScope.isOpen) { + hide(); + } + }) + ); - var unregisterTriggers = function () { - element.unbind(triggers.show, showTooltipBind); - element.unbind(triggers.hide, hideTooltipBind); + observers.push( + tooltipLinkedScope.$watch(function() { + if (!repositionScheduled) { + repositionScheduled = true; + tooltipLinkedScope.$$postDigest(function() { + repositionScheduled = false; + if (ttScope && ttScope.isOpen) { + positionTooltip(); + } + }); + } + }) + ); + } else { + observers.push( + attrs.$observe(ttType, function(val) { + ttScope.content = val; + if (!val && ttScope.isOpen) { + hide(); + } else { + positionTooltip(); + } + }) + ); + } + + observers.push( + attrs.$observe(prefix + 'Title', function(val) { + ttScope.title = val; + if (ttScope.isOpen) { + positionTooltip(); + } + }) + ); + + observers.push( + attrs.$observe(prefix + 'Placement', function(val) { + ttScope.placement = val ? val : options.placement; + if (ttScope.isOpen) { + positionTooltip(); + } + }) + ); + } + + function unregisterObservers() { + if (observers.length) { + angular.forEach(observers, function(observer) { + observer(); + }); + observers.length = 0; + } + } + + // hide tooltips/popovers for outsideClick trigger + function bodyHideTooltipBind(e) { + if (!ttScope || !ttScope.isOpen || !tooltip) { + return; + } + // make sure the tooltip/popover link or tool tooltip/popover itself were not clicked + if (!element[0].contains(e.target) && !tooltip[0].contains(e.target)) { + hideTooltipBind(); + } + } + + var unregisterTriggers = function() { + triggers.show.forEach(function(trigger) { + if (trigger === 'outsideClick') { + element.off('click', toggleTooltipBind); + } else { + element.off(trigger, showTooltipBind); + element.off(trigger, toggleTooltipBind); + } + }); + triggers.hide.forEach(function(trigger) { + if (trigger === 'outsideClick') { + $document.off('click', bodyHideTooltipBind); + } else { + element.off(trigger, hideTooltipBind); + } + }); }; - attrs.$observe( prefix+'Trigger', function ( val ) { + function prepTriggers() { + var val = attrs[prefix + 'Trigger']; unregisterTriggers(); - triggers = getTriggers( val ); + triggers = getTriggers(val); - if ( triggers.show === triggers.hide ) { - element.bind( triggers.show, toggleTooltipBind ); - } else { - element.bind( triggers.show, showTooltipBind ); - element.bind( triggers.hide, hideTooltipBind ); + if (triggers.show !== 'none') { + triggers.show.forEach(function(trigger, idx) { + if (trigger === 'outsideClick') { + element.on('click', toggleTooltipBind); + $document.on('click', bodyHideTooltipBind); + } else if (trigger === triggers.hide[idx]) { + element.on(trigger, toggleTooltipBind); + } else if (trigger) { + element.on(trigger, showTooltipBind); + element.on(triggers.hide[idx], hideTooltipBind); + } + + element.on('keypress', function(e) { + if (e.which === 27) { + hideTooltipBind(); + } + }); + }); } - }); + } + + prepTriggers(); var animation = scope.$eval(attrs[prefix + 'Animation']); - scope.tt_animation = angular.isDefined(animation) ? !!animation : options.animation; + ttScope.animation = angular.isDefined(animation) ? !!animation : options.animation; - attrs.$observe( prefix+'AppendToBody', function ( val ) { - appendToBody = angular.isDefined( val ) ? $parse( val )( scope ) : appendToBody; - }); - - // if a tooltip is attached to we need to remove it on - // location change as its parent scope will probably not be destroyed - // by the change. - if ( appendToBody ) { - scope.$on('$locationChangeSuccess', function closeTooltipOnLocationChangeSuccess () { - if ( scope.tt_isOpen ) { - hide(); - } - }); + var appendToBodyVal; + var appendKey = prefix + 'AppendToBody'; + if (appendKey in attrs && attrs[appendKey] === undefined) { + appendToBodyVal = true; + } else { + appendToBodyVal = scope.$eval(attrs[appendKey]); } + appendToBody = angular.isDefined(appendToBodyVal) ? appendToBodyVal : appendToBody; + // Make sure tooltip is destroyed and removed. scope.$on('$destroy', function onDestroyTooltip() { - $timeout.cancel( transitionTimeout ); - $timeout.cancel( popupTimeout ); unregisterTriggers(); removeTooltip(); + openedTooltips.remove(ttScope); + ttScope = null; }); }; } @@ -2678,172 +5024,371 @@ angular.module( 'ui.bootstrap.tooltip', [ 'ui.bootstrap.position', 'ui.bootstrap }]; }) -.directive( 'tooltipPopup', function () { +// This is mostly ngInclude code but with a custom scope +.directive('uibTooltipTemplateTransclude', [ + '$animate', '$sce', '$compile', '$templateRequest', +function ($animate, $sce, $compile, $templateRequest) { return { - restrict: 'EA', - replace: true, - scope: { content: '@', placement: '@', animation: '&', isOpen: '&' }, - templateUrl: 'template/tooltip/tooltip-popup.html' - }; -}) + link: function(scope, elem, attrs) { + var origScope = scope.$eval(attrs.tooltipTemplateTranscludeScope); -.directive( 'tooltip', [ '$tooltip', function ( $tooltip ) { - return $tooltip( 'tooltip', 'tooltip', 'mouseenter' ); + var changeCounter = 0, + currentScope, + previousElement, + currentElement; + + var cleanupLastIncludeContent = function() { + if (previousElement) { + previousElement.remove(); + previousElement = null; + } + + if (currentScope) { + currentScope.$destroy(); + currentScope = null; + } + + if (currentElement) { + $animate.leave(currentElement).then(function() { + previousElement = null; + }); + previousElement = currentElement; + currentElement = null; + } + }; + + scope.$watch($sce.parseAsResourceUrl(attrs.uibTooltipTemplateTransclude), function(src) { + var thisChangeId = ++changeCounter; + + if (src) { + //set the 2nd param to true to ignore the template request error so that the inner + //contents and scope can be cleaned up. + $templateRequest(src, true).then(function(response) { + if (thisChangeId !== changeCounter) { return; } + var newScope = origScope.$new(); + var template = response; + + var clone = $compile(template)(newScope, function(clone) { + cleanupLastIncludeContent(); + $animate.enter(clone, elem); + }); + + currentScope = newScope; + currentElement = clone; + + currentScope.$emit('$includeContentLoaded', src); + }, function() { + if (thisChangeId === changeCounter) { + cleanupLastIncludeContent(); + scope.$emit('$includeContentError', src); + } + }); + scope.$emit('$includeContentRequested', src); + } else { + cleanupLastIncludeContent(); + } + }); + + scope.$on('$destroy', cleanupLastIncludeContent); + } + }; }]) -.directive( 'tooltipHtmlUnsafePopup', function () { +/** + * Note that it's intentional that these classes are *not* applied through $animate. + * They must not be animated as they're expected to be present on the tooltip on + * initialization. + */ +.directive('uibTooltipClasses', ['$uibPosition', function($uibPosition) { + return { + restrict: 'A', + link: function(scope, element, attrs) { + // need to set the primary position so the + // arrow has space during position measure. + // tooltip.positionTooltip() + if (scope.placement) { + // // There are no top-left etc... classes + // // in TWBS, so we need the primary position. + var position = $uibPosition.parsePlacement(scope.placement); + element.addClass(position[0]); + } + + if (scope.popupClass) { + element.addClass(scope.popupClass); + } + + if (scope.animation()) { + element.addClass(attrs.tooltipAnimationClass); + } + } + }; +}]) + +.directive('uibTooltipPopup', function() { return { - restrict: 'EA', replace: true, - scope: { content: '@', placement: '@', animation: '&', isOpen: '&' }, - templateUrl: 'template/tooltip/tooltip-html-unsafe-popup.html' + scope: { content: '@', placement: '@', popupClass: '@', animation: '&', isOpen: '&' }, + templateUrl: 'uib/template/tooltip/tooltip-popup.html' }; }) -.directive( 'tooltipHtmlUnsafe', [ '$tooltip', function ( $tooltip ) { - return $tooltip( 'tooltipHtmlUnsafe', 'tooltip', 'mouseenter' ); +.directive('uibTooltip', [ '$uibTooltip', function($uibTooltip) { + return $uibTooltip('uibTooltip', 'tooltip', 'mouseenter'); +}]) + +.directive('uibTooltipTemplatePopup', function() { + return { + replace: true, + scope: { contentExp: '&', placement: '@', popupClass: '@', animation: '&', isOpen: '&', + originScope: '&' }, + templateUrl: 'uib/template/tooltip/tooltip-template-popup.html' + }; +}) + +.directive('uibTooltipTemplate', ['$uibTooltip', function($uibTooltip) { + return $uibTooltip('uibTooltipTemplate', 'tooltip', 'mouseenter', { + useContentExp: true + }); +}]) + +.directive('uibTooltipHtmlPopup', function() { + return { + replace: true, + scope: { contentExp: '&', placement: '@', popupClass: '@', animation: '&', isOpen: '&' }, + templateUrl: 'uib/template/tooltip/tooltip-html-popup.html' + }; +}) + +.directive('uibTooltipHtml', ['$uibTooltip', function($uibTooltip) { + return $uibTooltip('uibTooltipHtml', 'tooltip', 'mouseenter', { + useContentExp: true + }); }]); /** * The following features are still outstanding: popup delay, animation as a * function, placement as a function, inside, support for more triggers than - * just mouse enter/leave, html popovers, and selector delegatation. + * just mouse enter/leave, and selector delegatation. */ -angular.module( 'ui.bootstrap.popover', [ 'ui.bootstrap.tooltip' ] ) +angular.module('ui.bootstrap.popover', ['ui.bootstrap.tooltip']) -.directive( 'popoverPopup', function () { +.directive('uibPopoverTemplatePopup', function() { return { - restrict: 'EA', replace: true, - scope: { title: '@', content: '@', placement: '@', animation: '&', isOpen: '&' }, - templateUrl: 'template/popover/popover.html' + scope: { uibTitle: '@', contentExp: '&', placement: '@', popupClass: '@', animation: '&', isOpen: '&', + originScope: '&' }, + templateUrl: 'uib/template/popover/popover-template.html' }; }) -.directive( 'popover', [ '$tooltip', function ( $tooltip ) { - return $tooltip( 'popover', 'popover', 'click' ); +.directive('uibPopoverTemplate', ['$uibTooltip', function($uibTooltip) { + return $uibTooltip('uibPopoverTemplate', 'popover', 'click', { + useContentExp: true + }); +}]) + +.directive('uibPopoverHtmlPopup', function() { + return { + replace: true, + scope: { contentExp: '&', uibTitle: '@', placement: '@', popupClass: '@', animation: '&', isOpen: '&' }, + templateUrl: 'uib/template/popover/popover-html.html' + }; +}) + +.directive('uibPopoverHtml', ['$uibTooltip', function($uibTooltip) { + return $uibTooltip('uibPopoverHtml', 'popover', 'click', { + useContentExp: true + }); +}]) + +.directive('uibPopoverPopup', function() { + return { + replace: true, + scope: { uibTitle: '@', content: '@', placement: '@', popupClass: '@', animation: '&', isOpen: '&' }, + templateUrl: 'uib/template/popover/popover.html' + }; +}) + +.directive('uibPopover', ['$uibTooltip', function($uibTooltip) { + return $uibTooltip('uibPopover', 'popover', 'click'); }]); angular.module('ui.bootstrap.progressbar', []) -.constant('progressConfig', { +.constant('uibProgressConfig', { animate: true, max: 100 }) -.controller('ProgressController', ['$scope', '$attrs', 'progressConfig', function($scope, $attrs, progressConfig) { - var self = this, - animate = angular.isDefined($attrs.animate) ? $scope.$parent.$eval($attrs.animate) : progressConfig.animate; +.controller('UibProgressController', ['$scope', '$attrs', 'uibProgressConfig', function($scope, $attrs, progressConfig) { + var self = this, + animate = angular.isDefined($attrs.animate) ? $scope.$parent.$eval($attrs.animate) : progressConfig.animate; - this.bars = []; - $scope.max = angular.isDefined($attrs.max) ? $scope.$parent.$eval($attrs.max) : progressConfig.max; + this.bars = []; + $scope.max = getMaxOrDefault(); - this.addBar = function(bar, element) { - if ( !animate ) { - element.css({'transition': 'none'}); - } + this.addBar = function(bar, element, attrs) { + if (!animate) { + element.css({'transition': 'none'}); + } - this.bars.push(bar); + this.bars.push(bar); - bar.$watch('value', function( value ) { - bar.percent = +(100 * value / $scope.max).toFixed(2); - }); + bar.max = getMaxOrDefault(); + bar.title = attrs && angular.isDefined(attrs.title) ? attrs.title : 'progressbar'; - bar.$on('$destroy', function() { - element = null; - self.removeBar(bar); - }); + bar.$watch('value', function(value) { + bar.recalculatePercentage(); + }); + + bar.recalculatePercentage = function() { + var totalPercentage = self.bars.reduce(function(total, bar) { + bar.percent = +(100 * bar.value / bar.max).toFixed(2); + return total + bar.percent; + }, 0); + + if (totalPercentage > 100) { + bar.percent -= totalPercentage - 100; + } }; - this.removeBar = function(bar) { - this.bars.splice(this.bars.indexOf(bar), 1); - }; + bar.$on('$destroy', function() { + element = null; + self.removeBar(bar); + }); + }; + + this.removeBar = function(bar) { + this.bars.splice(this.bars.indexOf(bar), 1); + this.bars.forEach(function (bar) { + bar.recalculatePercentage(); + }); + }; + + //$attrs.$observe('maxParam', function(maxParam) { + $scope.$watch('maxParam', function(maxParam) { + self.bars.forEach(function(bar) { + bar.max = getMaxOrDefault(); + bar.recalculatePercentage(); + }); + }); + + function getMaxOrDefault () { + return angular.isDefined($scope.maxParam) ? $scope.maxParam : progressConfig.max; + } }]) -.directive('progress', function() { - return { - restrict: 'EA', - replace: true, - transclude: true, - controller: 'ProgressController', - require: 'progress', - scope: {}, - templateUrl: 'template/progressbar/progress.html' - }; +.directive('uibProgress', function() { + return { + replace: true, + transclude: true, + controller: 'UibProgressController', + require: 'uibProgress', + scope: { + maxParam: '=?max' + }, + templateUrl: 'uib/template/progressbar/progress.html' + }; }) -.directive('bar', function() { - return { - restrict: 'EA', - replace: true, - transclude: true, - require: '^progress', - scope: { - value: '=', - type: '@' - }, - templateUrl: 'template/progressbar/bar.html', - link: function(scope, element, attrs, progressCtrl) { - progressCtrl.addBar(scope, element); - } - }; +.directive('uibBar', function() { + return { + replace: true, + transclude: true, + require: '^uibProgress', + scope: { + value: '=', + type: '@' + }, + templateUrl: 'uib/template/progressbar/bar.html', + link: function(scope, element, attrs, progressCtrl) { + progressCtrl.addBar(scope, element, attrs); + } + }; }) -.directive('progressbar', function() { - return { - restrict: 'EA', - replace: true, - transclude: true, - controller: 'ProgressController', - scope: { - value: '=', - type: '@' - }, - templateUrl: 'template/progressbar/progressbar.html', - link: function(scope, element, attrs, progressCtrl) { - progressCtrl.addBar(scope, angular.element(element.children()[0])); - } - }; +.directive('uibProgressbar', function() { + return { + replace: true, + transclude: true, + controller: 'UibProgressController', + scope: { + value: '=', + maxParam: '=?max', + type: '@' + }, + templateUrl: 'uib/template/progressbar/progressbar.html', + link: function(scope, element, attrs, progressCtrl) { + progressCtrl.addBar(scope, angular.element(element.children()[0]), {title: attrs.title}); + } + }; }); + angular.module('ui.bootstrap.rating', []) -.constant('ratingConfig', { +.constant('uibRatingConfig', { max: 5, stateOn: null, - stateOff: null + stateOff: null, + enableReset: true, + titles : ['one', 'two', 'three', 'four', 'five'] }) -.controller('RatingController', ['$scope', '$attrs', 'ratingConfig', function($scope, $attrs, ratingConfig) { - var ngModelCtrl = { $setViewValue: angular.noop }; +.controller('UibRatingController', ['$scope', '$attrs', 'uibRatingConfig', function($scope, $attrs, ratingConfig) { + var ngModelCtrl = { $setViewValue: angular.noop }, + self = this; this.init = function(ngModelCtrl_) { ngModelCtrl = ngModelCtrl_; ngModelCtrl.$render = this.render; + ngModelCtrl.$formatters.push(function(value) { + if (angular.isNumber(value) && value << 0 !== value) { + value = Math.round(value); + } + + return value; + }); + this.stateOn = angular.isDefined($attrs.stateOn) ? $scope.$parent.$eval($attrs.stateOn) : ratingConfig.stateOn; this.stateOff = angular.isDefined($attrs.stateOff) ? $scope.$parent.$eval($attrs.stateOff) : ratingConfig.stateOff; + this.enableReset = angular.isDefined($attrs.enableReset) ? + $scope.$parent.$eval($attrs.enableReset) : ratingConfig.enableReset; + var tmpTitles = angular.isDefined($attrs.titles) ? $scope.$parent.$eval($attrs.titles) : ratingConfig.titles; + this.titles = angular.isArray(tmpTitles) && tmpTitles.length > 0 ? + tmpTitles : ratingConfig.titles; - var ratingStates = angular.isDefined($attrs.ratingStates) ? $scope.$parent.$eval($attrs.ratingStates) : - new Array( angular.isDefined($attrs.max) ? $scope.$parent.$eval($attrs.max) : ratingConfig.max ); + var ratingStates = angular.isDefined($attrs.ratingStates) ? + $scope.$parent.$eval($attrs.ratingStates) : + new Array(angular.isDefined($attrs.max) ? $scope.$parent.$eval($attrs.max) : ratingConfig.max); $scope.range = this.buildTemplateObjects(ratingStates); }; this.buildTemplateObjects = function(states) { for (var i = 0, n = states.length; i < n; i++) { - states[i] = angular.extend({ index: i }, { stateOn: this.stateOn, stateOff: this.stateOff }, states[i]); + states[i] = angular.extend({ index: i }, { stateOn: this.stateOn, stateOff: this.stateOff, title: this.getTitle(i) }, states[i]); } return states; }; + this.getTitle = function(index) { + if (index >= this.titles.length) { + return index + 1; + } + + return this.titles[index]; + }; + $scope.rate = function(value) { - if ( !$scope.readonly && value >= 0 && value <= $scope.range.length ) { - ngModelCtrl.$setViewValue(value); + if (!$scope.readonly && value >= 0 && value <= $scope.range.length) { + var newViewValue = self.enableReset && ngModelCtrl.$viewValue === value ? 0 : value; + ngModelCtrl.$setViewValue(newViewValue); ngModelCtrl.$render(); } }; $scope.enter = function(value) { - if ( !$scope.readonly ) { + if (!$scope.readonly) { $scope.value = value; } $scope.onHover({value: value}); @@ -2858,222 +5403,169 @@ angular.module('ui.bootstrap.rating', []) if (/(37|38|39|40)/.test(evt.which)) { evt.preventDefault(); evt.stopPropagation(); - $scope.rate( $scope.value + (evt.which === 38 || evt.which === 39 ? 1 : -1) ); + $scope.rate($scope.value + (evt.which === 38 || evt.which === 39 ? 1 : -1)); } }; this.render = function() { $scope.value = ngModelCtrl.$viewValue; + $scope.title = self.getTitle($scope.value - 1); }; }]) -.directive('rating', function() { +.directive('uibRating', function() { return { - restrict: 'EA', - require: ['rating', 'ngModel'], + require: ['uibRating', 'ngModel'], scope: { - readonly: '=?', + readonly: '=?readOnly', onHover: '&', onLeave: '&' }, - controller: 'RatingController', - templateUrl: 'template/rating/rating.html', + controller: 'UibRatingController', + templateUrl: 'uib/template/rating/rating.html', replace: true, link: function(scope, element, attrs, ctrls) { var ratingCtrl = ctrls[0], ngModelCtrl = ctrls[1]; - - if ( ngModelCtrl ) { - ratingCtrl.init( ngModelCtrl ); - } + ratingCtrl.init(ngModelCtrl); } }; }); -/** - * @ngdoc overview - * @name ui.bootstrap.tabs - * - * @description - * AngularJS version of the tabs directive. - */ - angular.module('ui.bootstrap.tabs', []) -.controller('TabsetController', ['$scope', function TabsetCtrl($scope) { +.controller('UibTabsetController', ['$scope', function ($scope) { var ctrl = this, - tabs = ctrl.tabs = $scope.tabs = []; + oldIndex; + ctrl.tabs = []; - ctrl.select = function(selectedTab) { - angular.forEach(tabs, function(tab) { - if (tab.active && tab !== selectedTab) { - tab.active = false; - tab.onDeselect(); + ctrl.select = function(index, evt) { + if (!destroyed) { + var previousIndex = findTabIndex(oldIndex); + var previousSelected = ctrl.tabs[previousIndex]; + if (previousSelected) { + previousSelected.tab.onDeselect({ + $event: evt + }); + if (evt && evt.isDefaultPrevented()) { + return; + } + previousSelected.tab.active = false; } - }); - selectedTab.active = true; - selectedTab.onSelect(); + + var selected = ctrl.tabs[index]; + if (selected) { + selected.tab.onSelect({ + $event: evt + }); + selected.tab.active = true; + ctrl.active = selected.index; + oldIndex = selected.index; + } else if (!selected && angular.isNumber(oldIndex)) { + ctrl.active = null; + oldIndex = null; + } + } }; ctrl.addTab = function addTab(tab) { - tabs.push(tab); - // we can't run the select function on the first tab - // since that would select it twice - if (tabs.length === 1) { - tab.active = true; - } else if (tab.active) { - ctrl.select(tab); + ctrl.tabs.push({ + tab: tab, + index: tab.index + }); + ctrl.tabs.sort(function(t1, t2) { + if (t1.index > t2.index) { + return 1; + } + + if (t1.index < t2.index) { + return -1; + } + + return 0; + }); + + if (tab.index === ctrl.active || !angular.isNumber(ctrl.active) && ctrl.tabs.length === 1) { + var newActiveIndex = findTabIndex(tab.index); + ctrl.select(newActiveIndex); } }; ctrl.removeTab = function removeTab(tab) { - var index = tabs.indexOf(tab); - //Select a new tab if the tab to be removed is selected - if (tab.active && tabs.length > 1) { - //If this is the last tab, select the previous tab. else, the next tab. - var newActiveIndex = index == tabs.length - 1 ? index - 1 : index + 1; - ctrl.select(tabs[newActiveIndex]); + var index; + for (var i = 0; i < ctrl.tabs.length; i++) { + if (ctrl.tabs[i].tab === tab) { + index = i; + break; + } } - tabs.splice(index, 1); + + if (ctrl.tabs[index].index === ctrl.active) { + var newActiveTabIndex = index === ctrl.tabs.length - 1 ? + index - 1 : index + 1 % ctrl.tabs.length; + ctrl.select(newActiveTabIndex); + } + + ctrl.tabs.splice(index, 1); }; + + $scope.$watch('tabset.active', function(val) { + if (angular.isNumber(val) && val !== oldIndex) { + ctrl.select(findTabIndex(val)); + } + }); + + var destroyed; + $scope.$on('$destroy', function() { + destroyed = true; + }); + + function findTabIndex(index) { + for (var i = 0; i < ctrl.tabs.length; i++) { + if (ctrl.tabs[i].index === index) { + return i; + } + } + } }]) -/** - * @ngdoc directive - * @name ui.bootstrap.tabs.directive:tabset - * @restrict EA - * - * @description - * Tabset is the outer container for the tabs directive - * - * @param {boolean=} vertical Whether or not to use vertical styling for the tabs. - * @param {boolean=} justified Whether or not to use justified styling for the tabs. - * - * @example - - - - First Content! - Second Content! - -
        - - First Vertical Content! - Second Vertical Content! - - - First Justified Content! - Second Justified Content! - -
        -
        - */ -.directive('tabset', function() { +.directive('uibTabset', function() { return { - restrict: 'EA', transclude: true, replace: true, - scope: { + scope: {}, + bindToController: { + active: '=?', type: '@' }, - controller: 'TabsetController', - templateUrl: 'template/tabs/tabset.html', + controller: 'UibTabsetController', + controllerAs: 'tabset', + templateUrl: function(element, attrs) { + return attrs.templateUrl || 'uib/template/tabs/tabset.html'; + }, link: function(scope, element, attrs) { - scope.vertical = angular.isDefined(attrs.vertical) ? scope.$parent.$eval(attrs.vertical) : false; - scope.justified = angular.isDefined(attrs.justified) ? scope.$parent.$eval(attrs.justified) : false; + scope.vertical = angular.isDefined(attrs.vertical) ? + scope.$parent.$eval(attrs.vertical) : false; + scope.justified = angular.isDefined(attrs.justified) ? + scope.$parent.$eval(attrs.justified) : false; + if (angular.isUndefined(attrs.active)) { + scope.active = 0; + } } }; }) -/** - * @ngdoc directive - * @name ui.bootstrap.tabs.directive:tab - * @restrict EA - * - * @param {string=} heading The visible heading, or title, of the tab. Set HTML headings with {@link ui.bootstrap.tabs.directive:tabHeading tabHeading}. - * @param {string=} select An expression to evaluate when the tab is selected. - * @param {boolean=} active A binding, telling whether or not this tab is selected. - * @param {boolean=} disabled A binding, telling whether or not this tab is disabled. - * - * @description - * Creates a tab with a heading and content. Must be placed within a {@link ui.bootstrap.tabs.directive:tabset tabset}. - * - * @example - - -
        - - -
        - - First Tab - - Alert me! - Second Tab, with alert callback and html heading! - - - {{item.content}} - - -
        -
        - - function TabsDemoCtrl($scope) { - $scope.items = [ - { title:"Dynamic Title 1", content:"Dynamic Item 0" }, - { title:"Dynamic Title 2", content:"Dynamic Item 1", disabled: true } - ]; - - $scope.alertMe = function() { - setTimeout(function() { - alert("You've selected the alert tab!"); - }); - }; - }; - -
        - */ - -/** - * @ngdoc directive - * @name ui.bootstrap.tabs.directive:tabHeading - * @restrict EA - * - * @description - * Creates an HTML heading for a {@link ui.bootstrap.tabs.directive:tab tab}. Must be placed as a child of a tab element. - * - * @example - - - - - HTML in my titles?! - And some content, too! - - - Icon heading?!? - That's right. - - - - - */ -.directive('tab', ['$parse', function($parse) { +.directive('uibTab', ['$parse', function($parse) { return { - require: '^tabset', - restrict: 'EA', + require: '^uibTabset', replace: true, - templateUrl: 'template/tabs/tab.html', + templateUrl: function(element, attrs) { + return attrs.templateUrl || 'uib/template/tabs/tab.html'; + }, transclude: true, scope: { - active: '=?', heading: '@', + index: '=?', + classes: '@?', onSelect: '&select', //This callback is called in contentHeadingTransclude //once it inserts the tab's content into the dom onDeselect: '&deselect' @@ -3081,45 +5573,58 @@ angular.module('ui.bootstrap.tabs', []) controller: function() { //Empty controller so other directives can require being 'under' a tab }, - compile: function(elm, attrs, transclude) { - return function postLink(scope, elm, attrs, tabsetCtrl) { - scope.$watch('active', function(active) { - if (active) { - tabsetCtrl.select(scope); - } + controllerAs: 'tab', + link: function(scope, elm, attrs, tabsetCtrl, transclude) { + scope.disabled = false; + if (attrs.disable) { + scope.$parent.$watch($parse(attrs.disable), function(value) { + scope.disabled = !! value; }); + } - scope.disabled = false; - if ( attrs.disabled ) { - scope.$parent.$watch($parse(attrs.disabled), function(value) { - scope.disabled = !! value; - }); + if (angular.isUndefined(attrs.index)) { + if (tabsetCtrl.tabs && tabsetCtrl.tabs.length) { + scope.index = Math.max.apply(null, tabsetCtrl.tabs.map(function(t) { return t.index; })) + 1; + } else { + scope.index = 0; } + } - scope.select = function() { - if ( !scope.disabled ) { - scope.active = true; + if (angular.isUndefined(attrs.classes)) { + scope.classes = ''; + } + + scope.select = function(evt) { + if (!scope.disabled) { + var index; + for (var i = 0; i < tabsetCtrl.tabs.length; i++) { + if (tabsetCtrl.tabs[i].tab === scope) { + index = i; + break; + } } - }; - tabsetCtrl.addTab(scope); - scope.$on('$destroy', function() { - tabsetCtrl.removeTab(scope); - }); - - //We need to transclude later, once the content container is ready. - //when this link happens, we're inside a tab heading. - scope.$transcludeFn = transclude; + tabsetCtrl.select(index, evt); + } }; + + tabsetCtrl.addTab(scope); + scope.$on('$destroy', function() { + tabsetCtrl.removeTab(scope); + }); + + //We need to transclude later, once the content container is ready. + //when this link happens, we're inside a tab heading. + scope.$transcludeFn = transclude; } }; }]) -.directive('tabHeadingTransclude', [function() { +.directive('uibTabHeadingTransclude', function() { return { restrict: 'A', - require: '^tab', - link: function(scope, elm, attrs, tabCtrl) { + require: '^uibTab', + link: function(scope, elm) { scope.$watch('headingElement', function updateHeadingElement(heading) { if (heading) { elm.html(''); @@ -3128,14 +5633,14 @@ angular.module('ui.bootstrap.tabs', []) }); } }; -}]) +}) -.directive('tabContentTransclude', function() { +.directive('uibTabContentTransclude', function() { return { restrict: 'A', - require: '^tabset', + require: '^uibTabset', link: function(scope, elm, attrs) { - var tab = scope.$eval(attrs.tabContentTransclude); + var tab = scope.$eval(attrs.uibTabContentTransclude).tab; //Now our tab is ready to be transcluded: both the tab heading area //and the tab content area are loaded. Transclude 'em both. @@ -3151,96 +5656,197 @@ angular.module('ui.bootstrap.tabs', []) }); } }; + function isTabHeading(node) { - return node.tagName && ( - node.hasAttribute('tab-heading') || - node.hasAttribute('data-tab-heading') || - node.tagName.toLowerCase() === 'tab-heading' || - node.tagName.toLowerCase() === 'data-tab-heading' + return node.tagName && ( + node.hasAttribute('uib-tab-heading') || + node.hasAttribute('data-uib-tab-heading') || + node.hasAttribute('x-uib-tab-heading') || + node.tagName.toLowerCase() === 'uib-tab-heading' || + node.tagName.toLowerCase() === 'data-uib-tab-heading' || + node.tagName.toLowerCase() === 'x-uib-tab-heading' || + node.tagName.toLowerCase() === 'uib:tab-heading' ); } -}) - -; +}); angular.module('ui.bootstrap.timepicker', []) -.constant('timepickerConfig', { +.constant('uibTimepickerConfig', { hourStep: 1, minuteStep: 1, + secondStep: 1, showMeridian: true, + showSeconds: false, meridians: null, readonlyInput: false, - mousewheel: true + mousewheel: true, + arrowkeys: true, + showSpinners: true, + templateUrl: 'uib/template/timepicker/timepicker.html' }) -.controller('TimepickerController', ['$scope', '$attrs', '$parse', '$log', '$locale', 'timepickerConfig', function($scope, $attrs, $parse, $log, $locale, timepickerConfig) { +.controller('UibTimepickerController', ['$scope', '$element', '$attrs', '$parse', '$log', '$locale', 'uibTimepickerConfig', function($scope, $element, $attrs, $parse, $log, $locale, timepickerConfig) { var selected = new Date(), - ngModelCtrl = { $setViewValue: angular.noop }, // nullModelCtrl - meridians = angular.isDefined($attrs.meridians) ? $scope.$parent.$eval($attrs.meridians) : timepickerConfig.meridians || $locale.DATETIME_FORMATS.AMPMS; + watchers = [], + ngModelCtrl = { $setViewValue: angular.noop }, // nullModelCtrl + meridians = angular.isDefined($attrs.meridians) ? $scope.$parent.$eval($attrs.meridians) : timepickerConfig.meridians || $locale.DATETIME_FORMATS.AMPMS, + padHours = angular.isDefined($attrs.padHours) ? $scope.$parent.$eval($attrs.padHours) : true; - this.init = function( ngModelCtrl_, inputs ) { + $scope.tabindex = angular.isDefined($attrs.tabindex) ? $attrs.tabindex : 0; + $element.removeAttr('tabindex'); + + this.init = function(ngModelCtrl_, inputs) { ngModelCtrl = ngModelCtrl_; ngModelCtrl.$render = this.render; + ngModelCtrl.$formatters.unshift(function(modelValue) { + return modelValue ? new Date(modelValue) : null; + }); + var hoursInputEl = inputs.eq(0), - minutesInputEl = inputs.eq(1); + minutesInputEl = inputs.eq(1), + secondsInputEl = inputs.eq(2); var mousewheel = angular.isDefined($attrs.mousewheel) ? $scope.$parent.$eval($attrs.mousewheel) : timepickerConfig.mousewheel; - if ( mousewheel ) { - this.setupMousewheelEvents( hoursInputEl, minutesInputEl ); + + if (mousewheel) { + this.setupMousewheelEvents(hoursInputEl, minutesInputEl, secondsInputEl); + } + + var arrowkeys = angular.isDefined($attrs.arrowkeys) ? $scope.$parent.$eval($attrs.arrowkeys) : timepickerConfig.arrowkeys; + if (arrowkeys) { + this.setupArrowkeyEvents(hoursInputEl, minutesInputEl, secondsInputEl); } $scope.readonlyInput = angular.isDefined($attrs.readonlyInput) ? $scope.$parent.$eval($attrs.readonlyInput) : timepickerConfig.readonlyInput; - this.setupInputEvents( hoursInputEl, minutesInputEl ); + this.setupInputEvents(hoursInputEl, minutesInputEl, secondsInputEl); }; var hourStep = timepickerConfig.hourStep; if ($attrs.hourStep) { - $scope.$parent.$watch($parse($attrs.hourStep), function(value) { - hourStep = parseInt(value, 10); - }); + watchers.push($scope.$parent.$watch($parse($attrs.hourStep), function(value) { + hourStep = +value; + })); } var minuteStep = timepickerConfig.minuteStep; if ($attrs.minuteStep) { - $scope.$parent.$watch($parse($attrs.minuteStep), function(value) { - minuteStep = parseInt(value, 10); - }); + watchers.push($scope.$parent.$watch($parse($attrs.minuteStep), function(value) { + minuteStep = +value; + })); + } + + var min; + watchers.push($scope.$parent.$watch($parse($attrs.min), function(value) { + var dt = new Date(value); + min = isNaN(dt) ? undefined : dt; + })); + + var max; + watchers.push($scope.$parent.$watch($parse($attrs.max), function(value) { + var dt = new Date(value); + max = isNaN(dt) ? undefined : dt; + })); + + var disabled = false; + if ($attrs.ngDisabled) { + watchers.push($scope.$parent.$watch($parse($attrs.ngDisabled), function(value) { + disabled = value; + })); + } + + $scope.noIncrementHours = function() { + var incrementedSelected = addMinutes(selected, hourStep * 60); + return disabled || incrementedSelected > max || + incrementedSelected < selected && incrementedSelected < min; + }; + + $scope.noDecrementHours = function() { + var decrementedSelected = addMinutes(selected, -hourStep * 60); + return disabled || decrementedSelected < min || + decrementedSelected > selected && decrementedSelected > max; + }; + + $scope.noIncrementMinutes = function() { + var incrementedSelected = addMinutes(selected, minuteStep); + return disabled || incrementedSelected > max || + incrementedSelected < selected && incrementedSelected < min; + }; + + $scope.noDecrementMinutes = function() { + var decrementedSelected = addMinutes(selected, -minuteStep); + return disabled || decrementedSelected < min || + decrementedSelected > selected && decrementedSelected > max; + }; + + $scope.noIncrementSeconds = function() { + var incrementedSelected = addSeconds(selected, secondStep); + return disabled || incrementedSelected > max || + incrementedSelected < selected && incrementedSelected < min; + }; + + $scope.noDecrementSeconds = function() { + var decrementedSelected = addSeconds(selected, -secondStep); + return disabled || decrementedSelected < min || + decrementedSelected > selected && decrementedSelected > max; + }; + + $scope.noToggleMeridian = function() { + if (selected.getHours() < 12) { + return disabled || addMinutes(selected, 12 * 60) > max; + } + + return disabled || addMinutes(selected, -12 * 60) < min; + }; + + var secondStep = timepickerConfig.secondStep; + if ($attrs.secondStep) { + watchers.push($scope.$parent.$watch($parse($attrs.secondStep), function(value) { + secondStep = +value; + })); + } + + $scope.showSeconds = timepickerConfig.showSeconds; + if ($attrs.showSeconds) { + watchers.push($scope.$parent.$watch($parse($attrs.showSeconds), function(value) { + $scope.showSeconds = !!value; + })); } // 12H / 24H mode $scope.showMeridian = timepickerConfig.showMeridian; if ($attrs.showMeridian) { - $scope.$parent.$watch($parse($attrs.showMeridian), function(value) { + watchers.push($scope.$parent.$watch($parse($attrs.showMeridian), function(value) { $scope.showMeridian = !!value; - if ( ngModelCtrl.$error.time ) { + if (ngModelCtrl.$error.time) { // Evaluate from template var hours = getHoursFromTemplate(), minutes = getMinutesFromTemplate(); - if (angular.isDefined( hours ) && angular.isDefined( minutes )) { - selected.setHours( hours ); + if (angular.isDefined(hours) && angular.isDefined(minutes)) { + selected.setHours(hours); refresh(); } } else { updateTemplate(); } - }); + })); } // Get $scope.hours in 24H mode if valid - function getHoursFromTemplate ( ) { - var hours = parseInt( $scope.hours, 10 ); - var valid = ( $scope.showMeridian ) ? (hours > 0 && hours < 13) : (hours >= 0 && hours < 24); - if ( !valid ) { + function getHoursFromTemplate() { + var hours = +$scope.hours; + var valid = $scope.showMeridian ? hours > 0 && hours < 13 : + hours >= 0 && hours < 24; + if (!valid || $scope.hours === '') { return undefined; } - if ( $scope.showMeridian ) { - if ( hours === 12 ) { + if ($scope.showMeridian) { + if (hours === 12) { hours = 0; } - if ( $scope.meridian === meridians[1] ) { + if ($scope.meridian === meridians[1]) { hours = hours + 12; } } @@ -3248,89 +5854,213 @@ angular.module('ui.bootstrap.timepicker', []) } function getMinutesFromTemplate() { - var minutes = parseInt($scope.minutes, 10); - return ( minutes >= 0 && minutes < 60 ) ? minutes : undefined; + var minutes = +$scope.minutes; + var valid = minutes >= 0 && minutes < 60; + if (!valid || $scope.minutes === '') { + return undefined; + } + return minutes; } - function pad( value ) { - return ( angular.isDefined(value) && value.toString().length < 2 ) ? '0' + value : value; + function getSecondsFromTemplate() { + var seconds = +$scope.seconds; + return seconds >= 0 && seconds < 60 ? seconds : undefined; + } + + function pad(value, noPad) { + if (value === null) { + return ''; + } + + return angular.isDefined(value) && value.toString().length < 2 && !noPad ? + '0' + value : value.toString(); } // Respond on mousewheel spin - this.setupMousewheelEvents = function( hoursInputEl, minutesInputEl ) { + this.setupMousewheelEvents = function(hoursInputEl, minutesInputEl, secondsInputEl) { var isScrollingUp = function(e) { if (e.originalEvent) { e = e.originalEvent; } //pick correct delta variable depending on event - var delta = (e.wheelDelta) ? e.wheelDelta : -e.deltaY; - return (e.detail || delta > 0); + var delta = e.wheelDelta ? e.wheelDelta : -e.deltaY; + return e.detail || delta > 0; }; hoursInputEl.bind('mousewheel wheel', function(e) { - $scope.$apply( (isScrollingUp(e)) ? $scope.incrementHours() : $scope.decrementHours() ); + if (!disabled) { + $scope.$apply(isScrollingUp(e) ? $scope.incrementHours() : $scope.decrementHours()); + } e.preventDefault(); }); minutesInputEl.bind('mousewheel wheel', function(e) { - $scope.$apply( (isScrollingUp(e)) ? $scope.incrementMinutes() : $scope.decrementMinutes() ); + if (!disabled) { + $scope.$apply(isScrollingUp(e) ? $scope.incrementMinutes() : $scope.decrementMinutes()); + } e.preventDefault(); }); + secondsInputEl.bind('mousewheel wheel', function(e) { + if (!disabled) { + $scope.$apply(isScrollingUp(e) ? $scope.incrementSeconds() : $scope.decrementSeconds()); + } + e.preventDefault(); + }); }; - this.setupInputEvents = function( hoursInputEl, minutesInputEl ) { - if ( $scope.readonlyInput ) { + // Respond on up/down arrowkeys + this.setupArrowkeyEvents = function(hoursInputEl, minutesInputEl, secondsInputEl) { + hoursInputEl.bind('keydown', function(e) { + if (!disabled) { + if (e.which === 38) { // up + e.preventDefault(); + $scope.incrementHours(); + $scope.$apply(); + } else if (e.which === 40) { // down + e.preventDefault(); + $scope.decrementHours(); + $scope.$apply(); + } + } + }); + + minutesInputEl.bind('keydown', function(e) { + if (!disabled) { + if (e.which === 38) { // up + e.preventDefault(); + $scope.incrementMinutes(); + $scope.$apply(); + } else if (e.which === 40) { // down + e.preventDefault(); + $scope.decrementMinutes(); + $scope.$apply(); + } + } + }); + + secondsInputEl.bind('keydown', function(e) { + if (!disabled) { + if (e.which === 38) { // up + e.preventDefault(); + $scope.incrementSeconds(); + $scope.$apply(); + } else if (e.which === 40) { // down + e.preventDefault(); + $scope.decrementSeconds(); + $scope.$apply(); + } + } + }); + }; + + this.setupInputEvents = function(hoursInputEl, minutesInputEl, secondsInputEl) { + if ($scope.readonlyInput) { $scope.updateHours = angular.noop; $scope.updateMinutes = angular.noop; + $scope.updateSeconds = angular.noop; return; } - var invalidate = function(invalidHours, invalidMinutes) { - ngModelCtrl.$setViewValue( null ); + var invalidate = function(invalidHours, invalidMinutes, invalidSeconds) { + ngModelCtrl.$setViewValue(null); ngModelCtrl.$setValidity('time', false); if (angular.isDefined(invalidHours)) { $scope.invalidHours = invalidHours; } + if (angular.isDefined(invalidMinutes)) { $scope.invalidMinutes = invalidMinutes; } + + if (angular.isDefined(invalidSeconds)) { + $scope.invalidSeconds = invalidSeconds; + } }; $scope.updateHours = function() { - var hours = getHoursFromTemplate(); + var hours = getHoursFromTemplate(), + minutes = getMinutesFromTemplate(); - if ( angular.isDefined(hours) ) { - selected.setHours( hours ); - refresh( 'h' ); + ngModelCtrl.$setDirty(); + + if (angular.isDefined(hours) && angular.isDefined(minutes)) { + selected.setHours(hours); + selected.setMinutes(minutes); + if (selected < min || selected > max) { + invalidate(true); + } else { + refresh('h'); + } } else { invalidate(true); } }; hoursInputEl.bind('blur', function(e) { - if ( !$scope.invalidHours && $scope.hours < 10) { - $scope.$apply( function() { - $scope.hours = pad( $scope.hours ); + ngModelCtrl.$setTouched(); + if (modelIsEmpty()) { + makeValid(); + } else if ($scope.hours === null || $scope.hours === '') { + invalidate(true); + } else if (!$scope.invalidHours && $scope.hours < 10) { + $scope.$apply(function() { + $scope.hours = pad($scope.hours, !padHours); }); } }); $scope.updateMinutes = function() { - var minutes = getMinutesFromTemplate(); + var minutes = getMinutesFromTemplate(), + hours = getHoursFromTemplate(); - if ( angular.isDefined(minutes) ) { - selected.setMinutes( minutes ); - refresh( 'm' ); + ngModelCtrl.$setDirty(); + + if (angular.isDefined(minutes) && angular.isDefined(hours)) { + selected.setHours(hours); + selected.setMinutes(minutes); + if (selected < min || selected > max) { + invalidate(undefined, true); + } else { + refresh('m'); + } } else { invalidate(undefined, true); } }; minutesInputEl.bind('blur', function(e) { - if ( !$scope.invalidMinutes && $scope.minutes < 10 ) { + ngModelCtrl.$setTouched(); + if (modelIsEmpty()) { + makeValid(); + } else if ($scope.minutes === null) { + invalidate(undefined, true); + } else if (!$scope.invalidMinutes && $scope.minutes < 10) { + $scope.$apply(function() { + $scope.minutes = pad($scope.minutes); + }); + } + }); + + $scope.updateSeconds = function() { + var seconds = getSecondsFromTemplate(); + + ngModelCtrl.$setDirty(); + + if (angular.isDefined(seconds)) { + selected.setSeconds(seconds); + refresh('s'); + } else { + invalidate(undefined, undefined, true); + } + }; + + secondsInputEl.bind('blur', function(e) { + if (modelIsEmpty()) { + makeValid(); + } else if (!$scope.invalidSeconds && $scope.seconds < 10) { $scope.$apply( function() { - $scope.minutes = pad( $scope.minutes ); + $scope.seconds = pad($scope.seconds); }); } }); @@ -3338,462 +6068,839 @@ angular.module('ui.bootstrap.timepicker', []) }; this.render = function() { - var date = ngModelCtrl.$modelValue ? new Date( ngModelCtrl.$modelValue ) : null; + var date = ngModelCtrl.$viewValue; - if ( isNaN(date) ) { + if (isNaN(date)) { ngModelCtrl.$setValidity('time', false); $log.error('Timepicker directive: "ng-model" value must be a Date object, a number of milliseconds since 01.01.1970 or a string representing an RFC2822 or ISO 8601 date.'); } else { - if ( date ) { + if (date) { selected = date; } - makeValid(); + + if (selected < min || selected > max) { + ngModelCtrl.$setValidity('time', false); + $scope.invalidHours = true; + $scope.invalidMinutes = true; + } else { + makeValid(); + } updateTemplate(); } }; // Call internally when we know that model is valid. - function refresh( keyboardChange ) { + function refresh(keyboardChange) { makeValid(); - ngModelCtrl.$setViewValue( new Date(selected) ); - updateTemplate( keyboardChange ); + ngModelCtrl.$setViewValue(new Date(selected)); + updateTemplate(keyboardChange); } function makeValid() { ngModelCtrl.$setValidity('time', true); $scope.invalidHours = false; $scope.invalidMinutes = false; + $scope.invalidSeconds = false; } - function updateTemplate( keyboardChange ) { - var hours = selected.getHours(), minutes = selected.getMinutes(); + function updateTemplate(keyboardChange) { + if (!ngModelCtrl.$modelValue) { + $scope.hours = null; + $scope.minutes = null; + $scope.seconds = null; + $scope.meridian = meridians[0]; + } else { + var hours = selected.getHours(), + minutes = selected.getMinutes(), + seconds = selected.getSeconds(); - if ( $scope.showMeridian ) { - hours = ( hours === 0 || hours === 12 ) ? 12 : hours % 12; // Convert 24 to 12 hour system + if ($scope.showMeridian) { + hours = hours === 0 || hours === 12 ? 12 : hours % 12; // Convert 24 to 12 hour system + } + + $scope.hours = keyboardChange === 'h' ? hours : pad(hours, !padHours); + if (keyboardChange !== 'm') { + $scope.minutes = pad(minutes); + } + $scope.meridian = selected.getHours() < 12 ? meridians[0] : meridians[1]; + + if (keyboardChange !== 's') { + $scope.seconds = pad(seconds); + } + $scope.meridian = selected.getHours() < 12 ? meridians[0] : meridians[1]; } - - $scope.hours = keyboardChange === 'h' ? hours : pad(hours); - $scope.minutes = keyboardChange === 'm' ? minutes : pad(minutes); - $scope.meridian = selected.getHours() < 12 ? meridians[0] : meridians[1]; } - function addMinutes( minutes ) { - var dt = new Date( selected.getTime() + minutes * 60000 ); - selected.setHours( dt.getHours(), dt.getMinutes() ); + function addSecondsToSelected(seconds) { + selected = addSeconds(selected, seconds); refresh(); } + function addMinutes(selected, minutes) { + return addSeconds(selected, minutes*60); + } + + function addSeconds(date, seconds) { + var dt = new Date(date.getTime() + seconds * 1000); + var newDate = new Date(date); + newDate.setHours(dt.getHours(), dt.getMinutes(), dt.getSeconds()); + return newDate; + } + + function modelIsEmpty() { + return ($scope.hours === null || $scope.hours === '') && + ($scope.minutes === null || $scope.minutes === '') && + (!$scope.showSeconds || $scope.showSeconds && ($scope.seconds === null || $scope.seconds === '')); + } + + $scope.showSpinners = angular.isDefined($attrs.showSpinners) ? + $scope.$parent.$eval($attrs.showSpinners) : timepickerConfig.showSpinners; + $scope.incrementHours = function() { - addMinutes( hourStep * 60 ); + if (!$scope.noIncrementHours()) { + addSecondsToSelected(hourStep * 60 * 60); + } }; + $scope.decrementHours = function() { - addMinutes( - hourStep * 60 ); + if (!$scope.noDecrementHours()) { + addSecondsToSelected(-hourStep * 60 * 60); + } }; + $scope.incrementMinutes = function() { - addMinutes( minuteStep ); + if (!$scope.noIncrementMinutes()) { + addSecondsToSelected(minuteStep * 60); + } }; + $scope.decrementMinutes = function() { - addMinutes( - minuteStep ); + if (!$scope.noDecrementMinutes()) { + addSecondsToSelected(-minuteStep * 60); + } }; + + $scope.incrementSeconds = function() { + if (!$scope.noIncrementSeconds()) { + addSecondsToSelected(secondStep); + } + }; + + $scope.decrementSeconds = function() { + if (!$scope.noDecrementSeconds()) { + addSecondsToSelected(-secondStep); + } + }; + $scope.toggleMeridian = function() { - addMinutes( 12 * 60 * (( selected.getHours() < 12 ) ? 1 : -1) ); - }; -}]) + var minutes = getMinutesFromTemplate(), + hours = getHoursFromTemplate(); -.directive('timepicker', function () { - return { - restrict: 'EA', - require: ['timepicker', '?^ngModel'], - controller:'TimepickerController', - replace: true, - scope: {}, - templateUrl: 'template/timepicker/timepicker.html', - link: function(scope, element, attrs, ctrls) { - var timepickerCtrl = ctrls[0], ngModelCtrl = ctrls[1]; - - if ( ngModelCtrl ) { - timepickerCtrl.init( ngModelCtrl, element.find('input') ); + if (!$scope.noToggleMeridian()) { + if (angular.isDefined(minutes) && angular.isDefined(hours)) { + addSecondsToSelected(12 * 60 * (selected.getHours() < 12 ? 60 : -60)); + } else { + $scope.meridian = $scope.meridian === meridians[0] ? meridians[1] : meridians[0]; } } }; -}); -angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap.bindHtml']) + $scope.blur = function() { + ngModelCtrl.$setTouched(); + }; + + $scope.$on('$destroy', function() { + while (watchers.length) { + watchers.shift()(); + } + }); +}]) + +.directive('uibTimepicker', ['uibTimepickerConfig', function(uibTimepickerConfig) { + return { + require: ['uibTimepicker', '?^ngModel'], + controller: 'UibTimepickerController', + controllerAs: 'timepicker', + replace: true, + scope: {}, + templateUrl: function(element, attrs) { + return attrs.templateUrl || uibTimepickerConfig.templateUrl; + }, + link: function(scope, element, attrs, ctrls) { + var timepickerCtrl = ctrls[0], ngModelCtrl = ctrls[1]; + + if (ngModelCtrl) { + timepickerCtrl.init(ngModelCtrl, element.find('input')); + } + } + }; +}]); + +angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.debounce', 'ui.bootstrap.position']) /** * A helper service that can parse typeahead's syntax (string provided by users) * Extracted to a separate service for ease of unit testing */ - .factory('typeaheadParser', ['$parse', function ($parse) { - - // 00000111000000000000022200000000000000003333333333333330000000000044000 - var TYPEAHEAD_REGEXP = /^\s*(.*?)(?:\s+as\s+(.*?))?\s+for\s+(?:([\$\w][\$\w\d]*))\s+in\s+(.*)$/; - - return { - parse:function (input) { - - var match = input.match(TYPEAHEAD_REGEXP); - if (!match) { - throw new Error( - 'Expected typeahead specification in form of "_modelValue_ (as _label_)? for _item_ in _collection_"' + - ' but got "' + input + '".'); - } - - return { - itemName:match[3], - source:$parse(match[4]), - viewMapper:$parse(match[2] || match[1]), - modelMapper:$parse(match[1]) - }; - } - }; -}]) - - .directive('typeahead', ['$compile', '$parse', '$q', '$timeout', '$document', '$position', 'typeaheadParser', - function ($compile, $parse, $q, $timeout, $document, $position, typeaheadParser) { - - var HOT_KEYS = [9, 13, 27, 38, 40]; - - return { - require:'ngModel', - link:function (originalScope, element, attrs, modelCtrl) { - - //SUPPORTED ATTRIBUTES (OPTIONS) - - //minimal no of characters that needs to be entered before typeahead kicks-in - var minSearch = originalScope.$eval(attrs.typeaheadMinLength) || 1; - - //minimal wait time after last character typed before typehead kicks-in - var waitTime = originalScope.$eval(attrs.typeaheadWaitMs) || 0; - - //should it restrict model values to the ones selected from the popup only? - var isEditable = originalScope.$eval(attrs.typeaheadEditable) !== false; - - //binding to a variable that indicates if matches are being retrieved asynchronously - var isLoadingSetter = $parse(attrs.typeaheadLoading).assign || angular.noop; - - //a callback executed when a match is selected - var onSelectCallback = $parse(attrs.typeaheadOnSelect); - - var inputFormatter = attrs.typeaheadInputFormatter ? $parse(attrs.typeaheadInputFormatter) : undefined; - - var appendToBody = attrs.typeaheadAppendToBody ? originalScope.$eval(attrs.typeaheadAppendToBody) : false; - - //INTERNAL VARIABLES - - //model setter executed upon match selection - var $setModelValue = $parse(attrs.ngModel).assign; - - //expressions used by typeahead - var parserResult = typeaheadParser.parse(attrs.typeahead); - - var hasFocus; - - //create a child scope for the typeahead directive so we are not polluting original scope - //with typeahead-specific data (matches, query etc.) - var scope = originalScope.$new(); - originalScope.$on('$destroy', function(){ - scope.$destroy(); - }); - - // WAI-ARIA - var popupId = 'typeahead-' + scope.$id + '-' + Math.floor(Math.random() * 10000); - element.attr({ - 'aria-autocomplete': 'list', - 'aria-expanded': false, - 'aria-owns': popupId - }); - - //pop-up element used to display matches - var popUpEl = angular.element('
        '); - popUpEl.attr({ - id: popupId, - matches: 'matches', - active: 'activeIdx', - select: 'select(activeIdx)', - query: 'query', - position: 'position' - }); - //custom item template - if (angular.isDefined(attrs.typeaheadTemplateUrl)) { - popUpEl.attr('template-url', attrs.typeaheadTemplateUrl); - } - - var resetMatches = function() { - scope.matches = []; - scope.activeIdx = -1; - element.attr('aria-expanded', false); - }; - - var getMatchId = function(index) { - return popupId + '-option-' + index; - }; - - // Indicate that the specified match is the active (pre-selected) item in the list owned by this typeahead. - // This attribute is added or removed automatically when the `activeIdx` changes. - scope.$watch('activeIdx', function(index) { - if (index < 0) { - element.removeAttr('aria-activedescendant'); - } else { - element.attr('aria-activedescendant', getMatchId(index)); + .factory('uibTypeaheadParser', ['$parse', function($parse) { + // 00000111000000000000022200000000000000003333333333333330000000000044000 + var TYPEAHEAD_REGEXP = /^\s*([\s\S]+?)(?:\s+as\s+([\s\S]+?))?\s+for\s+(?:([\$\w][\$\w\d]*))\s+in\s+([\s\S]+?)$/; + return { + parse: function(input) { + var match = input.match(TYPEAHEAD_REGEXP); + if (!match) { + throw new Error( + 'Expected typeahead specification in form of "_modelValue_ (as _label_)? for _item_ in _collection_"' + + ' but got "' + input + '".'); } + + return { + itemName: match[3], + source: $parse(match[4]), + viewMapper: $parse(match[2] || match[1]), + modelMapper: $parse(match[1]) + }; + } + }; + }]) + + .controller('UibTypeaheadController', ['$scope', '$element', '$attrs', '$compile', '$parse', '$q', '$timeout', '$document', '$window', '$rootScope', '$$debounce', '$uibPosition', 'uibTypeaheadParser', + function(originalScope, element, attrs, $compile, $parse, $q, $timeout, $document, $window, $rootScope, $$debounce, $position, typeaheadParser) { + var HOT_KEYS = [9, 13, 27, 38, 40]; + var eventDebounceTime = 200; + var modelCtrl, ngModelOptions; + //SUPPORTED ATTRIBUTES (OPTIONS) + + //minimal no of characters that needs to be entered before typeahead kicks-in + var minLength = originalScope.$eval(attrs.typeaheadMinLength); + if (!minLength && minLength !== 0) { + minLength = 1; + } + + originalScope.$watch(attrs.typeaheadMinLength, function (newVal) { + minLength = !newVal && newVal !== 0 ? 1 : newVal; + }); + + //minimal wait time after last character typed before typeahead kicks-in + var waitTime = originalScope.$eval(attrs.typeaheadWaitMs) || 0; + + //should it restrict model values to the ones selected from the popup only? + var isEditable = originalScope.$eval(attrs.typeaheadEditable) !== false; + originalScope.$watch(attrs.typeaheadEditable, function (newVal) { + isEditable = newVal !== false; + }); + + //binding to a variable that indicates if matches are being retrieved asynchronously + var isLoadingSetter = $parse(attrs.typeaheadLoading).assign || angular.noop; + + //a callback executed when a match is selected + var onSelectCallback = $parse(attrs.typeaheadOnSelect); + + //should it select highlighted popup value when losing focus? + var isSelectOnBlur = angular.isDefined(attrs.typeaheadSelectOnBlur) ? originalScope.$eval(attrs.typeaheadSelectOnBlur) : false; + + //binding to a variable that indicates if there were no results after the query is completed + var isNoResultsSetter = $parse(attrs.typeaheadNoResults).assign || angular.noop; + + var inputFormatter = attrs.typeaheadInputFormatter ? $parse(attrs.typeaheadInputFormatter) : undefined; + + var appendToBody = attrs.typeaheadAppendToBody ? originalScope.$eval(attrs.typeaheadAppendToBody) : false; + + var appendTo = attrs.typeaheadAppendTo ? + originalScope.$eval(attrs.typeaheadAppendTo) : null; + + var focusFirst = originalScope.$eval(attrs.typeaheadFocusFirst) !== false; + + //If input matches an item of the list exactly, select it automatically + var selectOnExact = attrs.typeaheadSelectOnExact ? originalScope.$eval(attrs.typeaheadSelectOnExact) : false; + + //binding to a variable that indicates if dropdown is open + var isOpenSetter = $parse(attrs.typeaheadIsOpen).assign || angular.noop; + + var showHint = originalScope.$eval(attrs.typeaheadShowHint) || false; + + //INTERNAL VARIABLES + + //model setter executed upon match selection + var parsedModel = $parse(attrs.ngModel); + var invokeModelSetter = $parse(attrs.ngModel + '($$$p)'); + var $setModelValue = function(scope, newValue) { + if (angular.isFunction(parsedModel(originalScope)) && + ngModelOptions && ngModelOptions.$options && ngModelOptions.$options.getterSetter) { + return invokeModelSetter(scope, {$$$p: newValue}); + } + + return parsedModel.assign(scope, newValue); + }; + + //expressions used by typeahead + var parserResult = typeaheadParser.parse(attrs.uibTypeahead); + + var hasFocus; + + //Used to avoid bug in iOS webview where iOS keyboard does not fire + //mousedown & mouseup events + //Issue #3699 + var selected; + + //create a child scope for the typeahead directive so we are not polluting original scope + //with typeahead-specific data (matches, query etc.) + var scope = originalScope.$new(); + var offDestroy = originalScope.$on('$destroy', function() { + scope.$destroy(); + }); + scope.$on('$destroy', offDestroy); + + // WAI-ARIA + var popupId = 'typeahead-' + scope.$id + '-' + Math.floor(Math.random() * 10000); + element.attr({ + 'aria-autocomplete': 'list', + 'aria-expanded': false, + 'aria-owns': popupId + }); + + var inputsContainer, hintInputElem; + //add read-only input to show hint + if (showHint) { + inputsContainer = angular.element('
        '); + inputsContainer.css('position', 'relative'); + element.after(inputsContainer); + hintInputElem = element.clone(); + hintInputElem.attr('placeholder', ''); + hintInputElem.attr('tabindex', '-1'); + hintInputElem.val(''); + hintInputElem.css({ + 'position': 'absolute', + 'top': '0px', + 'left': '0px', + 'border-color': 'transparent', + 'box-shadow': 'none', + 'opacity': 1, + 'background': 'none 0% 0% / auto repeat scroll padding-box border-box rgb(255, 255, 255)', + 'color': '#999' }); + element.css({ + 'position': 'relative', + 'vertical-align': 'top', + 'background-color': 'transparent' + }); + inputsContainer.append(hintInputElem); + hintInputElem.after(element); + } - var getMatchesAsync = function(inputValue) { + //pop-up element used to display matches + var popUpEl = angular.element('
        '); + popUpEl.attr({ + id: popupId, + matches: 'matches', + active: 'activeIdx', + select: 'select(activeIdx, evt)', + 'move-in-progress': 'moveInProgress', + query: 'query', + position: 'position', + 'assign-is-open': 'assignIsOpen(isOpen)', + debounce: 'debounceUpdate' + }); + //custom item template + if (angular.isDefined(attrs.typeaheadTemplateUrl)) { + popUpEl.attr('template-url', attrs.typeaheadTemplateUrl); + } - var locals = {$viewValue: inputValue}; - isLoadingSetter(originalScope, true); - $q.when(parserResult.source(originalScope, locals)).then(function(matches) { + if (angular.isDefined(attrs.typeaheadPopupTemplateUrl)) { + popUpEl.attr('popup-template-url', attrs.typeaheadPopupTemplateUrl); + } - //it might happen that several async queries were in progress if a user were typing fast - //but we are interested only in responses that correspond to the current view value - var onCurrentRequest = (inputValue === modelCtrl.$viewValue); - if (onCurrentRequest && hasFocus) { - if (matches.length > 0) { + var resetHint = function() { + if (showHint) { + hintInputElem.val(''); + } + }; - scope.activeIdx = 0; - scope.matches.length = 0; + var resetMatches = function() { + scope.matches = []; + scope.activeIdx = -1; + element.attr('aria-expanded', false); + resetHint(); + }; - //transform labels - for(var i=0; i index && inputValue) { + return inputValue.toUpperCase() === scope.matches[index].label.toUpperCase(); + } + + return false; + }; + + var getMatchesAsync = function(inputValue, evt) { + var locals = {$viewValue: inputValue}; + isLoadingSetter(originalScope, true); + isNoResultsSetter(originalScope, false); + $q.when(parserResult.source(originalScope, locals)).then(function(matches) { + //it might happen that several async queries were in progress if a user were typing fast + //but we are interested only in responses that correspond to the current view value + var onCurrentRequest = inputValue === modelCtrl.$viewValue; + if (onCurrentRequest && hasFocus) { + if (matches && matches.length > 0) { + scope.activeIdx = focusFirst ? 0 : -1; + isNoResultsSetter(originalScope, false); + scope.matches.length = 0; + + //transform labels + for (var i = 0; i < matches.length; i++) { + locals[parserResult.itemName] = matches[i]; + scope.matches.push({ + id: getMatchId(i), + label: parserResult.viewMapper(scope, locals), + model: matches[i] + }); } + + scope.query = inputValue; + //position pop-up with matches - we need to re-calculate its position each time we are opening a window + //with matches as a pop-up might be absolute-positioned and position of an input might have changed on a page + //due to other elements being rendered + recalculatePosition(); + + element.attr('aria-expanded', true); + + //Select the single remaining option if user input matches + if (selectOnExact && scope.matches.length === 1 && inputIsExactMatch(inputValue, 0)) { + if (angular.isNumber(scope.debounceUpdate) || angular.isObject(scope.debounceUpdate)) { + $$debounce(function() { + scope.select(0, evt); + }, angular.isNumber(scope.debounceUpdate) ? scope.debounceUpdate : scope.debounceUpdate['default']); + } else { + scope.select(0, evt); + } + } + + if (showHint) { + var firstLabel = scope.matches[0].label; + if (angular.isString(inputValue) && + inputValue.length > 0 && + firstLabel.slice(0, inputValue.length).toUpperCase() === inputValue.toUpperCase()) { + hintInputElem.val(inputValue + firstLabel.slice(inputValue.length)); + } else { + hintInputElem.val(''); + } + } + } else { + resetMatches(); + isNoResultsSetter(originalScope, true); } - if (onCurrentRequest) { - isLoadingSetter(originalScope, false); - } - }, function(){ - resetMatches(); + } + if (onCurrentRequest) { isLoadingSetter(originalScope, false); - }); - }; + } + }, function() { + resetMatches(); + isLoadingSetter(originalScope, false); + isNoResultsSetter(originalScope, true); + }); + }; + + // bind events only if appendToBody params exist - performance feature + if (appendToBody) { + angular.element($window).on('resize', fireRecalculating); + $document.find('body').on('scroll', fireRecalculating); + } + + // Declare the debounced function outside recalculating for + // proper debouncing + var debouncedRecalculate = $$debounce(function() { + // if popup is visible + if (scope.matches.length) { + recalculatePosition(); + } + + scope.moveInProgress = false; + }, eventDebounceTime); + + // Default progress type + scope.moveInProgress = false; + + function fireRecalculating() { + if (!scope.moveInProgress) { + scope.moveInProgress = true; + scope.$digest(); + } + + debouncedRecalculate(); + } + + // recalculate actual position and set new values to scope + // after digest loop is popup in right position + function recalculatePosition() { + scope.position = appendToBody ? $position.offset(element) : $position.position(element); + scope.position.top += element.prop('offsetHeight'); + } + + //we need to propagate user's query so we can higlight matches + scope.query = undefined; + + //Declare the timeout promise var outside the function scope so that stacked calls can be cancelled later + var timeoutPromise; + + var scheduleSearchWithTimeout = function(inputValue) { + timeoutPromise = $timeout(function() { + getMatchesAsync(inputValue); + }, waitTime); + }; + + var cancelPreviousTimeout = function() { + if (timeoutPromise) { + $timeout.cancel(timeoutPromise); + } + }; + + resetMatches(); + + scope.assignIsOpen = function (isOpen) { + isOpenSetter(originalScope, isOpen); + }; + + scope.select = function(activeIdx, evt) { + //called from within the $digest() cycle + var locals = {}; + var model, item; + + selected = true; + locals[parserResult.itemName] = item = scope.matches[activeIdx].model; + model = parserResult.modelMapper(originalScope, locals); + $setModelValue(originalScope, model); + modelCtrl.$setValidity('editable', true); + modelCtrl.$setValidity('parse', true); + + onSelectCallback(originalScope, { + $item: item, + $model: model, + $label: parserResult.viewMapper(originalScope, locals), + $event: evt + }); resetMatches(); - //we need to propagate user's query so we can higlight matches - scope.query = undefined; + //return focus to the input element if a match was selected via a mouse click event + // use timeout to avoid $rootScope:inprog error + if (scope.$eval(attrs.typeaheadFocusOnSelect) !== false) { + $timeout(function() { element[0].focus(); }, 0, false); + } + }; - //Declare the timeout promise var outside the function scope so that stacked calls can be cancelled later - var timeoutPromise; + //bind keyboard events: arrows up(38) / down(40), enter(13) and tab(9), esc(27) + element.on('keydown', function(evt) { + //typeahead is open and an "interesting" key was pressed + if (scope.matches.length === 0 || HOT_KEYS.indexOf(evt.which) === -1) { + return; + } + + /** + * if there's nothing selected (i.e. focusFirst) and enter or tab is hit + * or + * shift + tab is pressed to bring focus to the previous element + * then clear the results + */ + if (scope.activeIdx === -1 && (evt.which === 9 || evt.which === 13) || evt.which === 9 && !!evt.shiftKey) { + resetMatches(); + scope.$digest(); + return; + } + + evt.preventDefault(); + var target; + switch (evt.which) { + case 9: + case 13: + scope.$apply(function () { + if (angular.isNumber(scope.debounceUpdate) || angular.isObject(scope.debounceUpdate)) { + $$debounce(function() { + scope.select(scope.activeIdx, evt); + }, angular.isNumber(scope.debounceUpdate) ? scope.debounceUpdate : scope.debounceUpdate['default']); + } else { + scope.select(scope.activeIdx, evt); + } + }); + break; + case 27: + evt.stopPropagation(); + + resetMatches(); + originalScope.$digest(); + break; + case 38: + scope.activeIdx = (scope.activeIdx > 0 ? scope.activeIdx : scope.matches.length) - 1; + scope.$digest(); + target = popUpEl.find('li')[scope.activeIdx]; + target.parentNode.scrollTop = target.offsetTop; + break; + case 40: + scope.activeIdx = (scope.activeIdx + 1) % scope.matches.length; + scope.$digest(); + target = popUpEl.find('li')[scope.activeIdx]; + target.parentNode.scrollTop = target.offsetTop; + break; + } + }); + + element.bind('focus', function (evt) { + hasFocus = true; + if (minLength === 0 && !modelCtrl.$viewValue) { + $timeout(function() { + getMatchesAsync(modelCtrl.$viewValue, evt); + }, 0); + } + }); + + element.bind('blur', function(evt) { + if (isSelectOnBlur && scope.matches.length && scope.activeIdx !== -1 && !selected) { + selected = true; + scope.$apply(function() { + if (angular.isObject(scope.debounceUpdate) && angular.isNumber(scope.debounceUpdate.blur)) { + $$debounce(function() { + scope.select(scope.activeIdx, evt); + }, scope.debounceUpdate.blur); + } else { + scope.select(scope.activeIdx, evt); + } + }); + } + if (!isEditable && modelCtrl.$error.editable) { + modelCtrl.$setViewValue(); + // Reset validity as we are clearing + modelCtrl.$setValidity('editable', true); + modelCtrl.$setValidity('parse', true); + element.val(''); + } + hasFocus = false; + selected = false; + }); + + // Keep reference to click handler to unbind it. + var dismissClickHandler = function(evt) { + // Issue #3973 + // Firefox treats right click as a click on document + if (element[0] !== evt.target && evt.which !== 3 && scope.matches.length !== 0) { + resetMatches(); + if (!$rootScope.$$phase) { + originalScope.$digest(); + } + } + }; + + $document.on('click', dismissClickHandler); + + originalScope.$on('$destroy', function() { + $document.off('click', dismissClickHandler); + if (appendToBody || appendTo) { + $popup.remove(); + } + + if (appendToBody) { + angular.element($window).off('resize', fireRecalculating); + $document.find('body').off('scroll', fireRecalculating); + } + // Prevent jQuery cache memory leak + popUpEl.remove(); + + if (showHint) { + inputsContainer.remove(); + } + }); + + var $popup = $compile(popUpEl)(scope); + + if (appendToBody) { + $document.find('body').append($popup); + } else if (appendTo) { + angular.element(appendTo).eq(0).append($popup); + } else { + element.after($popup); + } + + this.init = function(_modelCtrl, _ngModelOptions) { + modelCtrl = _modelCtrl; + ngModelOptions = _ngModelOptions; + + scope.debounceUpdate = modelCtrl.$options && $parse(modelCtrl.$options.debounce)(originalScope); //plug into $parsers pipeline to open a typeahead on view changes initiated from DOM //$parsers kick-in on all the changes coming from the view as well as manually triggered by $setViewValue - modelCtrl.$parsers.unshift(function (inputValue) { - + modelCtrl.$parsers.unshift(function(inputValue) { hasFocus = true; - if (inputValue && inputValue.length >= minSearch) { + if (minLength === 0 || inputValue && inputValue.length >= minLength) { if (waitTime > 0) { - if (timeoutPromise) { - $timeout.cancel(timeoutPromise);//cancel previous timeout - } - timeoutPromise = $timeout(function () { - getMatchesAsync(inputValue); - }, waitTime); + cancelPreviousTimeout(); + scheduleSearchWithTimeout(inputValue); } else { getMatchesAsync(inputValue); } } else { isLoadingSetter(originalScope, false); + cancelPreviousTimeout(); resetMatches(); } if (isEditable) { return inputValue; - } else { - if (!inputValue) { - // Reset in case user had typed something previously. - modelCtrl.$setValidity('editable', true); - return inputValue; - } else { - modelCtrl.$setValidity('editable', false); - return undefined; - } } + + if (!inputValue) { + // Reset in case user had typed something previously. + modelCtrl.$setValidity('editable', true); + return null; + } + + modelCtrl.$setValidity('editable', false); + return undefined; }); - modelCtrl.$formatters.push(function (modelValue) { - + modelCtrl.$formatters.push(function(modelValue) { var candidateViewValue, emptyViewValue; var locals = {}; + // The validity may be set to false via $parsers (see above) if + // the model is restricted to selected values. If the model + // is set manually it is considered to be valid. + if (!isEditable) { + modelCtrl.$setValidity('editable', true); + } + if (inputFormatter) { - - locals['$model'] = modelValue; + locals.$model = modelValue; return inputFormatter(originalScope, locals); - - } else { - - //it might happen that we don't have enough info to properly render input value - //we need to check for this situation and simply return model value if we can't apply custom formatting - locals[parserResult.itemName] = modelValue; - candidateViewValue = parserResult.viewMapper(originalScope, locals); - locals[parserResult.itemName] = undefined; - emptyViewValue = parserResult.viewMapper(originalScope, locals); - - return candidateViewValue!== emptyViewValue ? candidateViewValue : modelValue; - } - }); - - scope.select = function (activeIdx) { - //called from within the $digest() cycle - var locals = {}; - var model, item; - - locals[parserResult.itemName] = item = scope.matches[activeIdx].model; - model = parserResult.modelMapper(originalScope, locals); - $setModelValue(originalScope, model); - modelCtrl.$setValidity('editable', true); - - onSelectCallback(originalScope, { - $item: item, - $model: model, - $label: parserResult.viewMapper(originalScope, locals) - }); - - resetMatches(); - - //return focus to the input element if a match was selected via a mouse click event - // use timeout to avoid $rootScope:inprog error - $timeout(function() { element[0].focus(); }, 0, false); - }; - - //bind keyboard events: arrows up(38) / down(40), enter(13) and tab(9), esc(27) - element.bind('keydown', function (evt) { - - //typeahead is open and an "interesting" key was pressed - if (scope.matches.length === 0 || HOT_KEYS.indexOf(evt.which) === -1) { - return; } - evt.preventDefault(); + //it might happen that we don't have enough info to properly render input value + //we need to check for this situation and simply return model value if we can't apply custom formatting + locals[parserResult.itemName] = modelValue; + candidateViewValue = parserResult.viewMapper(originalScope, locals); + locals[parserResult.itemName] = undefined; + emptyViewValue = parserResult.viewMapper(originalScope, locals); - if (evt.which === 40) { - scope.activeIdx = (scope.activeIdx + 1) % scope.matches.length; - scope.$digest(); - - } else if (evt.which === 38) { - scope.activeIdx = (scope.activeIdx ? scope.activeIdx : scope.matches.length) - 1; - scope.$digest(); - - } else if (evt.which === 13 || evt.which === 9) { - scope.$apply(function () { - scope.select(scope.activeIdx); - }); - - } else if (evt.which === 27) { - evt.stopPropagation(); - - resetMatches(); - scope.$digest(); - } + return candidateViewValue !== emptyViewValue ? candidateViewValue : modelValue; }); + }; + }]) - element.bind('blur', function (evt) { - hasFocus = false; - }); - - // Keep reference to click handler to unbind it. - var dismissClickHandler = function (evt) { - if (element[0] !== evt.target) { - resetMatches(); - scope.$digest(); - } - }; - - $document.bind('click', dismissClickHandler); - - originalScope.$on('$destroy', function(){ - $document.unbind('click', dismissClickHandler); - }); - - var $popup = $compile(popUpEl)(scope); - if ( appendToBody ) { - $document.find('body').append($popup); - } else { - element.after($popup); - } - } - }; - -}]) - - .directive('typeaheadPopup', function () { + .directive('uibTypeahead', function() { return { - restrict:'EA', - scope:{ - matches:'=', - query:'=', - active:'=', - position:'=', - select:'&' - }, - replace:true, - templateUrl:'template/typeahead/typeahead-popup.html', - link:function (scope, element, attrs) { - - scope.templateUrl = attrs.templateUrl; - - scope.isOpen = function () { - return scope.matches.length > 0; - }; - - scope.isActive = function (matchIdx) { - return scope.active == matchIdx; - }; - - scope.selectActive = function (matchIdx) { - scope.active = matchIdx; - }; - - scope.selectMatch = function (activeIdx) { - scope.select({activeIdx:activeIdx}); - }; + controller: 'UibTypeaheadController', + require: ['ngModel', '^?ngModelOptions', 'uibTypeahead'], + link: function(originalScope, element, attrs, ctrls) { + ctrls[2].init(ctrls[0], ctrls[1]); } }; }) - .directive('typeaheadMatch', ['$http', '$templateCache', '$compile', '$parse', function ($http, $templateCache, $compile, $parse) { + .directive('uibTypeaheadPopup', ['$$debounce', function($$debounce) { return { - restrict:'EA', - scope:{ - index:'=', - match:'=', - query:'=' + scope: { + matches: '=', + query: '=', + active: '=', + position: '&', + moveInProgress: '=', + select: '&', + assignIsOpen: '&', + debounce: '&' }, - link:function (scope, element, attrs) { - var tplUrl = $parse(attrs.templateUrl)(scope.$parent) || 'template/typeahead/typeahead-match.html'; - $http.get(tplUrl, {cache: $templateCache}).success(function(tplContent){ - element.replaceWith($compile(tplContent.trim())(scope)); + replace: true, + templateUrl: function(element, attrs) { + return attrs.popupTemplateUrl || 'uib/template/typeahead/typeahead-popup.html'; + }, + link: function(scope, element, attrs) { + scope.templateUrl = attrs.templateUrl; + + scope.isOpen = function() { + var isDropdownOpen = scope.matches.length > 0; + scope.assignIsOpen({ isOpen: isDropdownOpen }); + return isDropdownOpen; + }; + + scope.isActive = function(matchIdx) { + return scope.active === matchIdx; + }; + + scope.selectActive = function(matchIdx) { + scope.active = matchIdx; + }; + + scope.selectMatch = function(activeIdx, evt) { + var debounce = scope.debounce(); + if (angular.isNumber(debounce) || angular.isObject(debounce)) { + $$debounce(function() { + scope.select({activeIdx: activeIdx, evt: evt}); + }, angular.isNumber(debounce) ? debounce : debounce['default']); + } else { + scope.select({activeIdx: activeIdx, evt: evt}); + } + }; + } + }; + }]) + + .directive('uibTypeaheadMatch', ['$templateRequest', '$compile', '$parse', function($templateRequest, $compile, $parse) { + return { + scope: { + index: '=', + match: '=', + query: '=' + }, + link: function(scope, element, attrs) { + var tplUrl = $parse(attrs.templateUrl)(scope.$parent) || 'uib/template/typeahead/typeahead-match.html'; + $templateRequest(tplUrl).then(function(tplContent) { + var tplEl = angular.element(tplContent.trim()); + element.replaceWith(tplEl); + $compile(tplEl)(scope); }); } }; }]) - .filter('typeaheadHighlight', function() { + .filter('uibTypeaheadHighlight', ['$sce', '$injector', '$log', function($sce, $injector, $log) { + var isSanitizePresent; + isSanitizePresent = $injector.has('$sanitize'); function escapeRegexp(queryToEscape) { + // Regex: capture the whole query string and replace it with the string that will be used to match + // the results, for example if the capture is "a" the result will be \a return queryToEscape.replace(/([.?*+^$[\]\\(){}|-])/g, '\\$1'); } + function containsHtml(matchItem) { + return /<.*>/g.test(matchItem); + } + return function(matchItem, query) { - return query ? ('' + matchItem).replace(new RegExp(escapeRegexp(query), 'gi'), '$&') : matchItem; + if (!isSanitizePresent && containsHtml(matchItem)) { + $log.warn('Unsafe use of typeahead please use ngSanitize'); // Warn the user about the danger + } + matchItem = query ? ('' + matchItem).replace(new RegExp(escapeRegexp(query), 'gi'), '$&') : matchItem; // Replaces the capture string with a the same string inside of a "strong" tag + if (!isSanitizePresent) { + matchItem = $sce.trustAsHtml(matchItem); // If $sanitize is not present we pack the string in a $sce object for the ng-bind-html directive + } + return matchItem; }; - }); + }]); +angular.module('ui.bootstrap.carousel').run(function() {!angular.$$csp().noInlineStyle && !angular.$$uibCarouselCss && angular.element(document).find('head').prepend(''); angular.$$uibCarouselCss = true; }); +angular.module('ui.bootstrap.datepicker').run(function() {!angular.$$csp().noInlineStyle && !angular.$$uibDatepickerCss && angular.element(document).find('head').prepend(''); angular.$$uibDatepickerCss = true; }); +angular.module('ui.bootstrap.position').run(function() {!angular.$$csp().noInlineStyle && !angular.$$uibPositionCss && angular.element(document).find('head').prepend(''); angular.$$uibPositionCss = true; }); +angular.module('ui.bootstrap.datepickerPopup').run(function() {!angular.$$csp().noInlineStyle && !angular.$$uibDatepickerpopupCss && angular.element(document).find('head').prepend(''); angular.$$uibDatepickerpopupCss = true; }); +angular.module('ui.bootstrap.tooltip').run(function() {!angular.$$csp().noInlineStyle && !angular.$$uibTooltipCss && angular.element(document).find('head').prepend(''); angular.$$uibTooltipCss = true; }); +angular.module('ui.bootstrap.timepicker').run(function() {!angular.$$csp().noInlineStyle && !angular.$$uibTimepickerCss && angular.element(document).find('head').prepend(''); angular.$$uibTimepickerCss = true; }); +angular.module('ui.bootstrap.typeahead').run(function() {!angular.$$csp().noInlineStyle && !angular.$$uibTypeaheadCss && angular.element(document).find('head').prepend(''); angular.$$uibTypeaheadCss = true; }); \ No newline at end of file diff --git a/app/bower_components/angular-bootstrap/ui-bootstrap.min.js b/app/bower_components/angular-bootstrap/ui-bootstrap.min.js index 53fd24e..8cfd7f0 100644 --- a/app/bower_components/angular-bootstrap/ui-bootstrap.min.js +++ b/app/bower_components/angular-bootstrap/ui-bootstrap.min.js @@ -2,8 +2,9 @@ * angular-ui-bootstrap * http://angular-ui.github.io/bootstrap/ - * Version: 0.11.0 - 2014-05-01 + * Version: 1.3.2 - 2016-04-14 * License: MIT - */ -angular.module("ui.bootstrap",["ui.bootstrap.transition","ui.bootstrap.collapse","ui.bootstrap.accordion","ui.bootstrap.alert","ui.bootstrap.bindHtml","ui.bootstrap.buttons","ui.bootstrap.carousel","ui.bootstrap.dateparser","ui.bootstrap.position","ui.bootstrap.datepicker","ui.bootstrap.dropdown","ui.bootstrap.modal","ui.bootstrap.pagination","ui.bootstrap.tooltip","ui.bootstrap.popover","ui.bootstrap.progressbar","ui.bootstrap.rating","ui.bootstrap.tabs","ui.bootstrap.timepicker","ui.bootstrap.typeahead"]),angular.module("ui.bootstrap.transition",[]).factory("$transition",["$q","$timeout","$rootScope",function(a,b,c){function d(a){for(var b in a)if(void 0!==f.style[b])return a[b]}var e=function(d,f,g){g=g||{};var h=a.defer(),i=e[g.animation?"animationEndEventName":"transitionEndEventName"],j=function(){c.$apply(function(){d.unbind(i,j),h.resolve(d)})};return i&&d.bind(i,j),b(function(){angular.isString(f)?d.addClass(f):angular.isFunction(f)?f(d):angular.isObject(f)&&d.css(f),i||h.resolve(d)}),h.promise.cancel=function(){i&&d.unbind(i,j),h.reject("Transition cancelled")},h.promise},f=document.createElement("trans"),g={WebkitTransition:"webkitTransitionEnd",MozTransition:"transitionend",OTransition:"oTransitionEnd",transition:"transitionend"},h={WebkitTransition:"webkitAnimationEnd",MozTransition:"animationend",OTransition:"oAnimationEnd",transition:"animationend"};return e.transitionEndEventName=d(g),e.animationEndEventName=d(h),e}]),angular.module("ui.bootstrap.collapse",["ui.bootstrap.transition"]).directive("collapse",["$transition",function(a){return{link:function(b,c,d){function e(b){function d(){j===e&&(j=void 0)}var e=a(c,b);return j&&j.cancel(),j=e,e.then(d,d),e}function f(){k?(k=!1,g()):(c.removeClass("collapse").addClass("collapsing"),e({height:c[0].scrollHeight+"px"}).then(g))}function g(){c.removeClass("collapsing"),c.addClass("collapse in"),c.css({height:"auto"})}function h(){if(k)k=!1,i(),c.css({height:0});else{c.css({height:c[0].scrollHeight+"px"});{c[0].offsetWidth}c.removeClass("collapse in").addClass("collapsing"),e({height:0}).then(i)}}function i(){c.removeClass("collapsing"),c.addClass("collapse")}var j,k=!0;b.$watch(d.collapse,function(a){a?h():f()})}}}]),angular.module("ui.bootstrap.accordion",["ui.bootstrap.collapse"]).constant("accordionConfig",{closeOthers:!0}).controller("AccordionController",["$scope","$attrs","accordionConfig",function(a,b,c){this.groups=[],this.closeOthers=function(d){var e=angular.isDefined(b.closeOthers)?a.$eval(b.closeOthers):c.closeOthers;e&&angular.forEach(this.groups,function(a){a!==d&&(a.isOpen=!1)})},this.addGroup=function(a){var b=this;this.groups.push(a),a.$on("$destroy",function(){b.removeGroup(a)})},this.removeGroup=function(a){var b=this.groups.indexOf(a);-1!==b&&this.groups.splice(b,1)}}]).directive("accordion",function(){return{restrict:"EA",controller:"AccordionController",transclude:!0,replace:!1,templateUrl:"template/accordion/accordion.html"}}).directive("accordionGroup",function(){return{require:"^accordion",restrict:"EA",transclude:!0,replace:!0,templateUrl:"template/accordion/accordion-group.html",scope:{heading:"@",isOpen:"=?",isDisabled:"=?"},controller:function(){this.setHeading=function(a){this.heading=a}},link:function(a,b,c,d){d.addGroup(a),a.$watch("isOpen",function(b){b&&d.closeOthers(a)}),a.toggleOpen=function(){a.isDisabled||(a.isOpen=!a.isOpen)}}}}).directive("accordionHeading",function(){return{restrict:"EA",transclude:!0,template:"",replace:!0,require:"^accordionGroup",link:function(a,b,c,d,e){d.setHeading(e(a,function(){}))}}}).directive("accordionTransclude",function(){return{require:"^accordionGroup",link:function(a,b,c,d){a.$watch(function(){return d[c.accordionTransclude]},function(a){a&&(b.html(""),b.append(a))})}}}),angular.module("ui.bootstrap.alert",[]).controller("AlertController",["$scope","$attrs",function(a,b){a.closeable="close"in b}]).directive("alert",function(){return{restrict:"EA",controller:"AlertController",templateUrl:"template/alert/alert.html",transclude:!0,replace:!0,scope:{type:"@",close:"&"}}}),angular.module("ui.bootstrap.bindHtml",[]).directive("bindHtmlUnsafe",function(){return function(a,b,c){b.addClass("ng-binding").data("$binding",c.bindHtmlUnsafe),a.$watch(c.bindHtmlUnsafe,function(a){b.html(a||"")})}}),angular.module("ui.bootstrap.buttons",[]).constant("buttonConfig",{activeClass:"active",toggleEvent:"click"}).controller("ButtonsController",["buttonConfig",function(a){this.activeClass=a.activeClass||"active",this.toggleEvent=a.toggleEvent||"click"}]).directive("btnRadio",function(){return{require:["btnRadio","ngModel"],controller:"ButtonsController",link:function(a,b,c,d){var e=d[0],f=d[1];f.$render=function(){b.toggleClass(e.activeClass,angular.equals(f.$modelValue,a.$eval(c.btnRadio)))},b.bind(e.toggleEvent,function(){var d=b.hasClass(e.activeClass);(!d||angular.isDefined(c.uncheckable))&&a.$apply(function(){f.$setViewValue(d?null:a.$eval(c.btnRadio)),f.$render()})})}}}).directive("btnCheckbox",function(){return{require:["btnCheckbox","ngModel"],controller:"ButtonsController",link:function(a,b,c,d){function e(){return g(c.btnCheckboxTrue,!0)}function f(){return g(c.btnCheckboxFalse,!1)}function g(b,c){var d=a.$eval(b);return angular.isDefined(d)?d:c}var h=d[0],i=d[1];i.$render=function(){b.toggleClass(h.activeClass,angular.equals(i.$modelValue,e()))},b.bind(h.toggleEvent,function(){a.$apply(function(){i.$setViewValue(b.hasClass(h.activeClass)?f():e()),i.$render()})})}}}),angular.module("ui.bootstrap.carousel",["ui.bootstrap.transition"]).controller("CarouselController",["$scope","$timeout","$transition",function(a,b,c){function d(){e();var c=+a.interval;!isNaN(c)&&c>=0&&(g=b(f,c))}function e(){g&&(b.cancel(g),g=null)}function f(){h?(a.next(),d()):a.pause()}var g,h,i=this,j=i.slides=a.slides=[],k=-1;i.currentSlide=null;var l=!1;i.select=a.select=function(e,f){function g(){if(!l){if(i.currentSlide&&angular.isString(f)&&!a.noTransition&&e.$element){e.$element.addClass(f);{e.$element[0].offsetWidth}angular.forEach(j,function(a){angular.extend(a,{direction:"",entering:!1,leaving:!1,active:!1})}),angular.extend(e,{direction:f,active:!0,entering:!0}),angular.extend(i.currentSlide||{},{direction:f,leaving:!0}),a.$currentTransition=c(e.$element,{}),function(b,c){a.$currentTransition.then(function(){h(b,c)},function(){h(b,c)})}(e,i.currentSlide)}else h(e,i.currentSlide);i.currentSlide=e,k=m,d()}}function h(b,c){angular.extend(b,{direction:"",active:!0,leaving:!1,entering:!1}),angular.extend(c||{},{direction:"",active:!1,leaving:!1,entering:!1}),a.$currentTransition=null}var m=j.indexOf(e);void 0===f&&(f=m>k?"next":"prev"),e&&e!==i.currentSlide&&(a.$currentTransition?(a.$currentTransition.cancel(),b(g)):g())},a.$on("$destroy",function(){l=!0}),i.indexOfSlide=function(a){return j.indexOf(a)},a.next=function(){var b=(k+1)%j.length;return a.$currentTransition?void 0:i.select(j[b],"next")},a.prev=function(){var b=0>k-1?j.length-1:k-1;return a.$currentTransition?void 0:i.select(j[b],"prev")},a.isActive=function(a){return i.currentSlide===a},a.$watch("interval",d),a.$on("$destroy",e),a.play=function(){h||(h=!0,d())},a.pause=function(){a.noPause||(h=!1,e())},i.addSlide=function(b,c){b.$element=c,j.push(b),1===j.length||b.active?(i.select(j[j.length-1]),1==j.length&&a.play()):b.active=!1},i.removeSlide=function(a){var b=j.indexOf(a);j.splice(b,1),j.length>0&&a.active?i.select(b>=j.length?j[b-1]:j[b]):k>b&&k--}}]).directive("carousel",[function(){return{restrict:"EA",transclude:!0,replace:!0,controller:"CarouselController",require:"carousel",templateUrl:"template/carousel/carousel.html",scope:{interval:"=",noTransition:"=",noPause:"="}}}]).directive("slide",function(){return{require:"^carousel",restrict:"EA",transclude:!0,replace:!0,templateUrl:"template/carousel/slide.html",scope:{active:"=?"},link:function(a,b,c,d){d.addSlide(a,b),a.$on("$destroy",function(){d.removeSlide(a)}),a.$watch("active",function(b){b&&d.select(a)})}}}),angular.module("ui.bootstrap.dateparser",[]).service("dateParser",["$locale","orderByFilter",function(a,b){function c(a,b,c){return 1===b&&c>28?29===c&&(a%4===0&&a%100!==0||a%400===0):3===b||5===b||8===b||10===b?31>c:!0}this.parsers={};var d={yyyy:{regex:"\\d{4}",apply:function(a){this.year=+a}},yy:{regex:"\\d{2}",apply:function(a){this.year=+a+2e3}},y:{regex:"\\d{1,4}",apply:function(a){this.year=+a}},MMMM:{regex:a.DATETIME_FORMATS.MONTH.join("|"),apply:function(b){this.month=a.DATETIME_FORMATS.MONTH.indexOf(b)}},MMM:{regex:a.DATETIME_FORMATS.SHORTMONTH.join("|"),apply:function(b){this.month=a.DATETIME_FORMATS.SHORTMONTH.indexOf(b)}},MM:{regex:"0[1-9]|1[0-2]",apply:function(a){this.month=a-1}},M:{regex:"[1-9]|1[0-2]",apply:function(a){this.month=a-1}},dd:{regex:"[0-2][0-9]{1}|3[0-1]{1}",apply:function(a){this.date=+a}},d:{regex:"[1-2]?[0-9]{1}|3[0-1]{1}",apply:function(a){this.date=+a}},EEEE:{regex:a.DATETIME_FORMATS.DAY.join("|")},EEE:{regex:a.DATETIME_FORMATS.SHORTDAY.join("|")}};this.createParser=function(a){var c=[],e=a.split("");return angular.forEach(d,function(b,d){var f=a.indexOf(d);if(f>-1){a=a.split(""),e[f]="("+b.regex+")",a[f]="$";for(var g=f+1,h=f+d.length;h>g;g++)e[g]="",a[g]="$";a=a.join(""),c.push({index:f,apply:b.apply})}}),{regex:new RegExp("^"+e.join("")+"$"),map:b(c,"index")}},this.parse=function(b,d){if(!angular.isString(b))return b;d=a.DATETIME_FORMATS[d]||d,this.parsers[d]||(this.parsers[d]=this.createParser(d));var e=this.parsers[d],f=e.regex,g=e.map,h=b.match(f);if(h&&h.length){for(var i,j={year:1900,month:0,date:1,hours:0},k=1,l=h.length;l>k;k++){var m=g[k-1];m.apply&&m.apply.call(j,h[k])}return c(j.year,j.month,j.date)&&(i=new Date(j.year,j.month,j.date,j.hours)),i}}}]),angular.module("ui.bootstrap.position",[]).factory("$position",["$document","$window",function(a,b){function c(a,c){return a.currentStyle?a.currentStyle[c]:b.getComputedStyle?b.getComputedStyle(a)[c]:a.style[c]}function d(a){return"static"===(c(a,"position")||"static")}var e=function(b){for(var c=a[0],e=b.offsetParent||c;e&&e!==c&&d(e);)e=e.offsetParent;return e||c};return{position:function(b){var c=this.offset(b),d={top:0,left:0},f=e(b[0]);f!=a[0]&&(d=this.offset(angular.element(f)),d.top+=f.clientTop-f.scrollTop,d.left+=f.clientLeft-f.scrollLeft);var g=b[0].getBoundingClientRect();return{width:g.width||b.prop("offsetWidth"),height:g.height||b.prop("offsetHeight"),top:c.top-d.top,left:c.left-d.left}},offset:function(c){var d=c[0].getBoundingClientRect();return{width:d.width||c.prop("offsetWidth"),height:d.height||c.prop("offsetHeight"),top:d.top+(b.pageYOffset||a[0].documentElement.scrollTop),left:d.left+(b.pageXOffset||a[0].documentElement.scrollLeft)}},positionElements:function(a,b,c,d){var e,f,g,h,i=c.split("-"),j=i[0],k=i[1]||"center";e=d?this.offset(a):this.position(a),f=b.prop("offsetWidth"),g=b.prop("offsetHeight");var l={center:function(){return e.left+e.width/2-f/2},left:function(){return e.left},right:function(){return e.left+e.width}},m={center:function(){return e.top+e.height/2-g/2},top:function(){return e.top},bottom:function(){return e.top+e.height}};switch(j){case"right":h={top:m[k](),left:l[j]()};break;case"left":h={top:m[k](),left:e.left-f};break;case"bottom":h={top:m[j](),left:l[k]()};break;default:h={top:e.top-g,left:l[k]()}}return h}}}]),angular.module("ui.bootstrap.datepicker",["ui.bootstrap.dateparser","ui.bootstrap.position"]).constant("datepickerConfig",{formatDay:"dd",formatMonth:"MMMM",formatYear:"yyyy",formatDayHeader:"EEE",formatDayTitle:"MMMM yyyy",formatMonthTitle:"yyyy",datepickerMode:"day",minMode:"day",maxMode:"year",showWeeks:!0,startingDay:0,yearRange:20,minDate:null,maxDate:null}).controller("DatepickerController",["$scope","$attrs","$parse","$interpolate","$timeout","$log","dateFilter","datepickerConfig",function(a,b,c,d,e,f,g,h){var i=this,j={$setViewValue:angular.noop};this.modes=["day","month","year"],angular.forEach(["formatDay","formatMonth","formatYear","formatDayHeader","formatDayTitle","formatMonthTitle","minMode","maxMode","showWeeks","startingDay","yearRange"],function(c,e){i[c]=angular.isDefined(b[c])?8>e?d(b[c])(a.$parent):a.$parent.$eval(b[c]):h[c]}),angular.forEach(["minDate","maxDate"],function(d){b[d]?a.$parent.$watch(c(b[d]),function(a){i[d]=a?new Date(a):null,i.refreshView()}):i[d]=h[d]?new Date(h[d]):null}),a.datepickerMode=a.datepickerMode||h.datepickerMode,a.uniqueId="datepicker-"+a.$id+"-"+Math.floor(1e4*Math.random()),this.activeDate=angular.isDefined(b.initDate)?a.$parent.$eval(b.initDate):new Date,a.isActive=function(b){return 0===i.compare(b.date,i.activeDate)?(a.activeDateId=b.uid,!0):!1},this.init=function(a){j=a,j.$render=function(){i.render()}},this.render=function(){if(j.$modelValue){var a=new Date(j.$modelValue),b=!isNaN(a);b?this.activeDate=a:f.error('Datepicker directive: "ng-model" value must be a Date object, a number of milliseconds since 01.01.1970 or a string representing an RFC2822 or ISO 8601 date.'),j.$setValidity("date",b)}this.refreshView()},this.refreshView=function(){if(this.element){this._refreshView();var a=j.$modelValue?new Date(j.$modelValue):null;j.$setValidity("date-disabled",!a||this.element&&!this.isDisabled(a))}},this.createDateObject=function(a,b){var c=j.$modelValue?new Date(j.$modelValue):null;return{date:a,label:g(a,b),selected:c&&0===this.compare(a,c),disabled:this.isDisabled(a),current:0===this.compare(a,new Date)}},this.isDisabled=function(c){return this.minDate&&this.compare(c,this.minDate)<0||this.maxDate&&this.compare(c,this.maxDate)>0||b.dateDisabled&&a.dateDisabled({date:c,mode:a.datepickerMode})},this.split=function(a,b){for(var c=[];a.length>0;)c.push(a.splice(0,b));return c},a.select=function(b){if(a.datepickerMode===i.minMode){var c=j.$modelValue?new Date(j.$modelValue):new Date(0,0,0,0,0,0,0);c.setFullYear(b.getFullYear(),b.getMonth(),b.getDate()),j.$setViewValue(c),j.$render()}else i.activeDate=b,a.datepickerMode=i.modes[i.modes.indexOf(a.datepickerMode)-1]},a.move=function(a){var b=i.activeDate.getFullYear()+a*(i.step.years||0),c=i.activeDate.getMonth()+a*(i.step.months||0);i.activeDate.setFullYear(b,c,1),i.refreshView()},a.toggleMode=function(b){b=b||1,a.datepickerMode===i.maxMode&&1===b||a.datepickerMode===i.minMode&&-1===b||(a.datepickerMode=i.modes[i.modes.indexOf(a.datepickerMode)+b])},a.keys={13:"enter",32:"space",33:"pageup",34:"pagedown",35:"end",36:"home",37:"left",38:"up",39:"right",40:"down"};var k=function(){e(function(){i.element[0].focus()},0,!1)};a.$on("datepicker.focus",k),a.keydown=function(b){var c=a.keys[b.which];if(c&&!b.shiftKey&&!b.altKey)if(b.preventDefault(),b.stopPropagation(),"enter"===c||"space"===c){if(i.isDisabled(i.activeDate))return;a.select(i.activeDate),k()}else!b.ctrlKey||"up"!==c&&"down"!==c?(i.handleKeyDown(c,b),i.refreshView()):(a.toggleMode("up"===c?1:-1),k())}}]).directive("datepicker",function(){return{restrict:"EA",replace:!0,templateUrl:"template/datepicker/datepicker.html",scope:{datepickerMode:"=?",dateDisabled:"&"},require:["datepicker","?^ngModel"],controller:"DatepickerController",link:function(a,b,c,d){var e=d[0],f=d[1];f&&e.init(f)}}}).directive("daypicker",["dateFilter",function(a){return{restrict:"EA",replace:!0,templateUrl:"template/datepicker/day.html",require:"^datepicker",link:function(b,c,d,e){function f(a,b){return 1!==b||a%4!==0||a%100===0&&a%400!==0?i[b]:29}function g(a,b){var c=new Array(b),d=new Date(a),e=0;for(d.setHours(12);b>e;)c[e++]=new Date(d),d.setDate(d.getDate()+1);return c}function h(a){var b=new Date(a);b.setDate(b.getDate()+4-(b.getDay()||7));var c=b.getTime();return b.setMonth(0),b.setDate(1),Math.floor(Math.round((c-b)/864e5)/7)+1}b.showWeeks=e.showWeeks,e.step={months:1},e.element=c;var i=[31,28,31,30,31,30,31,31,30,31,30,31];e._refreshView=function(){var c=e.activeDate.getFullYear(),d=e.activeDate.getMonth(),f=new Date(c,d,1),i=e.startingDay-f.getDay(),j=i>0?7-i:-i,k=new Date(f);j>0&&k.setDate(-j+1);for(var l=g(k,42),m=0;42>m;m++)l[m]=angular.extend(e.createDateObject(l[m],e.formatDay),{secondary:l[m].getMonth()!==d,uid:b.uniqueId+"-"+m});b.labels=new Array(7);for(var n=0;7>n;n++)b.labels[n]={abbr:a(l[n].date,e.formatDayHeader),full:a(l[n].date,"EEEE")};if(b.title=a(e.activeDate,e.formatDayTitle),b.rows=e.split(l,7),b.showWeeks){b.weekNumbers=[];for(var o=h(b.rows[0][0].date),p=b.rows.length;b.weekNumbers.push(o++)f;f++)c[f]=angular.extend(e.createDateObject(new Date(d,f,1),e.formatMonth),{uid:b.uniqueId+"-"+f});b.title=a(e.activeDate,e.formatMonthTitle),b.rows=e.split(c,3)},e.compare=function(a,b){return new Date(a.getFullYear(),a.getMonth())-new Date(b.getFullYear(),b.getMonth())},e.handleKeyDown=function(a){var b=e.activeDate.getMonth();if("left"===a)b-=1;else if("up"===a)b-=3;else if("right"===a)b+=1;else if("down"===a)b+=3;else if("pageup"===a||"pagedown"===a){var c=e.activeDate.getFullYear()+("pageup"===a?-1:1);e.activeDate.setFullYear(c)}else"home"===a?b=0:"end"===a&&(b=11);e.activeDate.setMonth(b)},e.refreshView()}}}]).directive("yearpicker",["dateFilter",function(){return{restrict:"EA",replace:!0,templateUrl:"template/datepicker/year.html",require:"^datepicker",link:function(a,b,c,d){function e(a){return parseInt((a-1)/f,10)*f+1}var f=d.yearRange;d.step={years:f},d.element=b,d._refreshView=function(){for(var b=new Array(f),c=0,g=e(d.activeDate.getFullYear());f>c;c++)b[c]=angular.extend(d.createDateObject(new Date(g+c,0,1),d.formatYear),{uid:a.uniqueId+"-"+c});a.title=[b[0].label,b[f-1].label].join(" - "),a.rows=d.split(b,5)},d.compare=function(a,b){return a.getFullYear()-b.getFullYear()},d.handleKeyDown=function(a){var b=d.activeDate.getFullYear();"left"===a?b-=1:"up"===a?b-=5:"right"===a?b+=1:"down"===a?b+=5:"pageup"===a||"pagedown"===a?b+=("pageup"===a?-1:1)*d.step.years:"home"===a?b=e(d.activeDate.getFullYear()):"end"===a&&(b=e(d.activeDate.getFullYear())+f-1),d.activeDate.setFullYear(b)},d.refreshView()}}}]).constant("datepickerPopupConfig",{datepickerPopup:"yyyy-MM-dd",currentText:"Today",clearText:"Clear",closeText:"Done",closeOnDateSelection:!0,appendToBody:!1,showButtonBar:!0}).directive("datepickerPopup",["$compile","$parse","$document","$position","dateFilter","dateParser","datepickerPopupConfig",function(a,b,c,d,e,f,g){return{restrict:"EA",require:"ngModel",scope:{isOpen:"=?",currentText:"@",clearText:"@",closeText:"@",dateDisabled:"&"},link:function(h,i,j,k){function l(a){return a.replace(/([A-Z])/g,function(a){return"-"+a.toLowerCase()})}function m(a){if(a){if(angular.isDate(a)&&!isNaN(a))return k.$setValidity("date",!0),a;if(angular.isString(a)){var b=f.parse(a,n)||new Date(a);return isNaN(b)?void k.$setValidity("date",!1):(k.$setValidity("date",!0),b)}return void k.$setValidity("date",!1)}return k.$setValidity("date",!0),null}var n,o=angular.isDefined(j.closeOnDateSelection)?h.$parent.$eval(j.closeOnDateSelection):g.closeOnDateSelection,p=angular.isDefined(j.datepickerAppendToBody)?h.$parent.$eval(j.datepickerAppendToBody):g.appendToBody;h.showButtonBar=angular.isDefined(j.showButtonBar)?h.$parent.$eval(j.showButtonBar):g.showButtonBar,h.getText=function(a){return h[a+"Text"]||g[a+"Text"]},j.$observe("datepickerPopup",function(a){n=a||g.datepickerPopup,k.$render()});var q=angular.element("
        ");q.attr({"ng-model":"date","ng-change":"dateSelection()"});var r=angular.element(q.children()[0]);j.datepickerOptions&&angular.forEach(h.$parent.$eval(j.datepickerOptions),function(a,b){r.attr(l(b),a)}),angular.forEach(["minDate","maxDate"],function(a){j[a]&&(h.$parent.$watch(b(j[a]),function(b){h[a]=b}),r.attr(l(a),a))}),j.dateDisabled&&r.attr("date-disabled","dateDisabled({ date: date, mode: mode })"),k.$parsers.unshift(m),h.dateSelection=function(a){angular.isDefined(a)&&(h.date=a),k.$setViewValue(h.date),k.$render(),o&&(h.isOpen=!1,i[0].focus())},i.bind("input change keyup",function(){h.$apply(function(){h.date=k.$modelValue})}),k.$render=function(){var a=k.$viewValue?e(k.$viewValue,n):"";i.val(a),h.date=m(k.$modelValue)};var s=function(a){h.isOpen&&a.target!==i[0]&&h.$apply(function(){h.isOpen=!1})},t=function(a){h.keydown(a)};i.bind("keydown",t),h.keydown=function(a){27===a.which?(a.preventDefault(),a.stopPropagation(),h.close()):40!==a.which||h.isOpen||(h.isOpen=!0)},h.$watch("isOpen",function(a){a?(h.$broadcast("datepicker.focus"),h.position=p?d.offset(i):d.position(i),h.position.top=h.position.top+i.prop("offsetHeight"),c.bind("click",s)):c.unbind("click",s)}),h.select=function(a){if("today"===a){var b=new Date;angular.isDate(k.$modelValue)?(a=new Date(k.$modelValue),a.setFullYear(b.getFullYear(),b.getMonth(),b.getDate())):a=new Date(b.setHours(0,0,0,0))}h.dateSelection(a)},h.close=function(){h.isOpen=!1,i[0].focus()};var u=a(q)(h);p?c.find("body").append(u):i.after(u),h.$on("$destroy",function(){u.remove(),i.unbind("keydown",t),c.unbind("click",s)})}}}]).directive("datepickerPopupWrap",function(){return{restrict:"EA",replace:!0,transclude:!0,templateUrl:"template/datepicker/popup.html",link:function(a,b){b.bind("click",function(a){a.preventDefault(),a.stopPropagation()})}}}),angular.module("ui.bootstrap.dropdown",[]).constant("dropdownConfig",{openClass:"open"}).service("dropdownService",["$document",function(a){var b=null;this.open=function(e){b||(a.bind("click",c),a.bind("keydown",d)),b&&b!==e&&(b.isOpen=!1),b=e},this.close=function(e){b===e&&(b=null,a.unbind("click",c),a.unbind("keydown",d))};var c=function(a){a&&a.isDefaultPrevented()||b.$apply(function(){b.isOpen=!1})},d=function(a){27===a.which&&(b.focusToggleElement(),c())}}]).controller("DropdownController",["$scope","$attrs","$parse","dropdownConfig","dropdownService","$animate",function(a,b,c,d,e,f){var g,h=this,i=a.$new(),j=d.openClass,k=angular.noop,l=b.onToggle?c(b.onToggle):angular.noop;this.init=function(d){h.$element=d,b.isOpen&&(g=c(b.isOpen),k=g.assign,a.$watch(g,function(a){i.isOpen=!!a}))},this.toggle=function(a){return i.isOpen=arguments.length?!!a:!i.isOpen},this.isOpen=function(){return i.isOpen},i.focusToggleElement=function(){h.toggleElement&&h.toggleElement[0].focus()},i.$watch("isOpen",function(b,c){f[b?"addClass":"removeClass"](h.$element,j),b?(i.focusToggleElement(),e.open(i)):e.close(i),k(a,b),angular.isDefined(b)&&b!==c&&l(a,{open:!!b})}),a.$on("$locationChangeSuccess",function(){i.isOpen=!1}),a.$on("$destroy",function(){i.$destroy()})}]).directive("dropdown",function(){return{restrict:"CA",controller:"DropdownController",link:function(a,b,c,d){d.init(b)}}}).directive("dropdownToggle",function(){return{restrict:"CA",require:"?^dropdown",link:function(a,b,c,d){if(d){d.toggleElement=b;var e=function(e){e.preventDefault(),b.hasClass("disabled")||c.disabled||a.$apply(function(){d.toggle()})};b.bind("click",e),b.attr({"aria-haspopup":!0,"aria-expanded":!1}),a.$watch(d.isOpen,function(a){b.attr("aria-expanded",!!a)}),a.$on("$destroy",function(){b.unbind("click",e)})}}}}),angular.module("ui.bootstrap.modal",["ui.bootstrap.transition"]).factory("$$stackedMap",function(){return{createNew:function(){var a=[];return{add:function(b,c){a.push({key:b,value:c})},get:function(b){for(var c=0;c0),i()})}function i(){if(k&&-1==g()){var a=l;j(k,l,150,function(){a.$destroy(),a=null}),k=void 0,l=void 0}}function j(c,d,e,f){function g(){g.done||(g.done=!0,c.remove(),f&&f())}d.animate=!1;var h=a.transitionEndEventName;if(h){var i=b(g,e);c.bind(h,function(){b.cancel(i),g(),d.$apply()})}else b(g,0)}var k,l,m="modal-open",n=f.createNew(),o={};return e.$watch(g,function(a){l&&(l.index=a)}),c.bind("keydown",function(a){var b;27===a.which&&(b=n.top(),b&&b.value.keyboard&&(a.preventDefault(),e.$apply(function(){o.dismiss(b.key,"escape key press")})))}),o.open=function(a,b){n.add(a,{deferred:b.deferred,modalScope:b.scope,backdrop:b.backdrop,keyboard:b.keyboard});var f=c.find("body").eq(0),h=g();h>=0&&!k&&(l=e.$new(!0),l.index=h,k=d("
        ")(l),f.append(k));var i=angular.element("
        ");i.attr({"template-url":b.windowTemplateUrl,"window-class":b.windowClass,size:b.size,index:n.length()-1,animate:"animate"}).html(b.content);var j=d(i)(b.scope);n.top().value.modalDomEl=j,f.append(j),f.addClass(m)},o.close=function(a,b){var c=n.get(a).value;c&&(c.deferred.resolve(b),h(a))},o.dismiss=function(a,b){var c=n.get(a).value;c&&(c.deferred.reject(b),h(a))},o.dismissAll=function(a){for(var b=this.getTop();b;)this.dismiss(b.key,a),b=this.getTop()},o.getTop=function(){return n.top()},o}]).provider("$modal",function(){var a={options:{backdrop:!0,keyboard:!0},$get:["$injector","$rootScope","$q","$http","$templateCache","$controller","$modalStack",function(b,c,d,e,f,g,h){function i(a){return a.template?d.when(a.template):e.get(a.templateUrl,{cache:f}).then(function(a){return a.data})}function j(a){var c=[];return angular.forEach(a,function(a){(angular.isFunction(a)||angular.isArray(a))&&c.push(d.when(b.invoke(a)))}),c}var k={};return k.open=function(b){var e=d.defer(),f=d.defer(),k={result:e.promise,opened:f.promise,close:function(a){h.close(k,a)},dismiss:function(a){h.dismiss(k,a)}};if(b=angular.extend({},a.options,b),b.resolve=b.resolve||{},!b.template&&!b.templateUrl)throw new Error("One of template or templateUrl options is required.");var l=d.all([i(b)].concat(j(b.resolve)));return l.then(function(a){var d=(b.scope||c).$new();d.$close=k.close,d.$dismiss=k.dismiss;var f,i={},j=1;b.controller&&(i.$scope=d,i.$modalInstance=k,angular.forEach(b.resolve,function(b,c){i[c]=a[j++]}),f=g(b.controller,i)),h.open(k,{scope:d,deferred:e,content:a[0],backdrop:b.backdrop,keyboard:b.keyboard,windowClass:b.windowClass,windowTemplateUrl:b.windowTemplateUrl,size:b.size})},function(a){e.reject(a)}),l.then(function(){f.resolve(!0)},function(){f.reject(!1)}),k},k}]};return a}),angular.module("ui.bootstrap.pagination",[]).controller("PaginationController",["$scope","$attrs","$parse",function(a,b,c){var d=this,e={$setViewValue:angular.noop},f=b.numPages?c(b.numPages).assign:angular.noop;this.init=function(f,g){e=f,this.config=g,e.$render=function(){d.render()},b.itemsPerPage?a.$parent.$watch(c(b.itemsPerPage),function(b){d.itemsPerPage=parseInt(b,10),a.totalPages=d.calculateTotalPages()}):this.itemsPerPage=g.itemsPerPage},this.calculateTotalPages=function(){var b=this.itemsPerPage<1?1:Math.ceil(a.totalItems/this.itemsPerPage);return Math.max(b||0,1)},this.render=function(){a.page=parseInt(e.$viewValue,10)||1},a.selectPage=function(b){a.page!==b&&b>0&&b<=a.totalPages&&(e.$setViewValue(b),e.$render())},a.getText=function(b){return a[b+"Text"]||d.config[b+"Text"]},a.noPrevious=function(){return 1===a.page},a.noNext=function(){return a.page===a.totalPages},a.$watch("totalItems",function(){a.totalPages=d.calculateTotalPages()}),a.$watch("totalPages",function(b){f(a.$parent,b),a.page>b?a.selectPage(b):e.$render()})}]).constant("paginationConfig",{itemsPerPage:10,boundaryLinks:!1,directionLinks:!0,firstText:"First",previousText:"Previous",nextText:"Next",lastText:"Last",rotate:!0}).directive("pagination",["$parse","paginationConfig",function(a,b){return{restrict:"EA",scope:{totalItems:"=",firstText:"@",previousText:"@",nextText:"@",lastText:"@"},require:["pagination","?ngModel"],controller:"PaginationController",templateUrl:"template/pagination/pagination.html",replace:!0,link:function(c,d,e,f){function g(a,b,c){return{number:a,text:b,active:c}}function h(a,b){var c=[],d=1,e=b,f=angular.isDefined(k)&&b>k;f&&(l?(d=Math.max(a-Math.floor(k/2),1),e=d+k-1,e>b&&(e=b,d=e-k+1)):(d=(Math.ceil(a/k)-1)*k+1,e=Math.min(d+k-1,b)));for(var h=d;e>=h;h++){var i=g(h,h,h===a);c.push(i)}if(f&&!l){if(d>1){var j=g(d-1,"...",!1);c.unshift(j)}if(b>e){var m=g(e+1,"...",!1);c.push(m)}}return c}var i=f[0],j=f[1];if(j){var k=angular.isDefined(e.maxSize)?c.$parent.$eval(e.maxSize):b.maxSize,l=angular.isDefined(e.rotate)?c.$parent.$eval(e.rotate):b.rotate;c.boundaryLinks=angular.isDefined(e.boundaryLinks)?c.$parent.$eval(e.boundaryLinks):b.boundaryLinks,c.directionLinks=angular.isDefined(e.directionLinks)?c.$parent.$eval(e.directionLinks):b.directionLinks,i.init(j,b),e.maxSize&&c.$parent.$watch(a(e.maxSize),function(a){k=parseInt(a,10),i.render()});var m=i.render;i.render=function(){m(),c.page>0&&c.page<=c.totalPages&&(c.pages=h(c.page,c.totalPages))}}}}}]).constant("pagerConfig",{itemsPerPage:10,previousText:"« Previous",nextText:"Next »",align:!0}).directive("pager",["pagerConfig",function(a){return{restrict:"EA",scope:{totalItems:"=",previousText:"@",nextText:"@"},require:["pager","?ngModel"],controller:"PaginationController",templateUrl:"template/pagination/pager.html",replace:!0,link:function(b,c,d,e){var f=e[0],g=e[1];g&&(b.align=angular.isDefined(d.align)?b.$parent.$eval(d.align):a.align,f.init(g,a))}}}]),angular.module("ui.bootstrap.tooltip",["ui.bootstrap.position","ui.bootstrap.bindHtml"]).provider("$tooltip",function(){function a(a){var b=/[A-Z]/g,c="-";return a.replace(b,function(a,b){return(b?c:"")+a.toLowerCase()})}var b={placement:"top",animation:!0,popupDelay:0},c={mouseenter:"mouseleave",click:"click",focus:"blur"},d={};this.options=function(a){angular.extend(d,a)},this.setTriggers=function(a){angular.extend(c,a)},this.$get=["$window","$compile","$timeout","$parse","$document","$position","$interpolate",function(e,f,g,h,i,j,k){return function(e,l,m){function n(a){var b=a||o.trigger||m,d=c[b]||b;return{show:b,hide:d}}var o=angular.extend({},b,d),p=a(e),q=k.startSymbol(),r=k.endSymbol(),s="
        ';return{restrict:"EA",scope:!0,compile:function(){var a=f(s);return function(b,c,d){function f(){b.tt_isOpen?m():k()}function k(){(!y||b.$eval(d[l+"Enable"]))&&(b.tt_popupDelay?v||(v=g(p,b.tt_popupDelay,!1),v.then(function(a){a() -})):p()())}function m(){b.$apply(function(){q()})}function p(){return v=null,u&&(g.cancel(u),u=null),b.tt_content?(r(),t.css({top:0,left:0,display:"block"}),w?i.find("body").append(t):c.after(t),z(),b.tt_isOpen=!0,b.$digest(),z):angular.noop}function q(){b.tt_isOpen=!1,g.cancel(v),v=null,b.tt_animation?u||(u=g(s,500)):s()}function r(){t&&s(),t=a(b,function(){}),b.$digest()}function s(){u=null,t&&(t.remove(),t=null)}var t,u,v,w=angular.isDefined(o.appendToBody)?o.appendToBody:!1,x=n(void 0),y=angular.isDefined(d[l+"Enable"]),z=function(){var a=j.positionElements(c,t,b.tt_placement,w);a.top+="px",a.left+="px",t.css(a)};b.tt_isOpen=!1,d.$observe(e,function(a){b.tt_content=a,!a&&b.tt_isOpen&&q()}),d.$observe(l+"Title",function(a){b.tt_title=a}),d.$observe(l+"Placement",function(a){b.tt_placement=angular.isDefined(a)?a:o.placement}),d.$observe(l+"PopupDelay",function(a){var c=parseInt(a,10);b.tt_popupDelay=isNaN(c)?o.popupDelay:c});var A=function(){c.unbind(x.show,k),c.unbind(x.hide,m)};d.$observe(l+"Trigger",function(a){A(),x=n(a),x.show===x.hide?c.bind(x.show,f):(c.bind(x.show,k),c.bind(x.hide,m))});var B=b.$eval(d[l+"Animation"]);b.tt_animation=angular.isDefined(B)?!!B:o.animation,d.$observe(l+"AppendToBody",function(a){w=angular.isDefined(a)?h(a)(b):w}),w&&b.$on("$locationChangeSuccess",function(){b.tt_isOpen&&q()}),b.$on("$destroy",function(){g.cancel(u),g.cancel(v),A(),s()})}}}}}]}).directive("tooltipPopup",function(){return{restrict:"EA",replace:!0,scope:{content:"@",placement:"@",animation:"&",isOpen:"&"},templateUrl:"template/tooltip/tooltip-popup.html"}}).directive("tooltip",["$tooltip",function(a){return a("tooltip","tooltip","mouseenter")}]).directive("tooltipHtmlUnsafePopup",function(){return{restrict:"EA",replace:!0,scope:{content:"@",placement:"@",animation:"&",isOpen:"&"},templateUrl:"template/tooltip/tooltip-html-unsafe-popup.html"}}).directive("tooltipHtmlUnsafe",["$tooltip",function(a){return a("tooltipHtmlUnsafe","tooltip","mouseenter")}]),angular.module("ui.bootstrap.popover",["ui.bootstrap.tooltip"]).directive("popoverPopup",function(){return{restrict:"EA",replace:!0,scope:{title:"@",content:"@",placement:"@",animation:"&",isOpen:"&"},templateUrl:"template/popover/popover.html"}}).directive("popover",["$tooltip",function(a){return a("popover","popover","click")}]),angular.module("ui.bootstrap.progressbar",[]).constant("progressConfig",{animate:!0,max:100}).controller("ProgressController",["$scope","$attrs","progressConfig",function(a,b,c){var d=this,e=angular.isDefined(b.animate)?a.$parent.$eval(b.animate):c.animate;this.bars=[],a.max=angular.isDefined(b.max)?a.$parent.$eval(b.max):c.max,this.addBar=function(b,c){e||c.css({transition:"none"}),this.bars.push(b),b.$watch("value",function(c){b.percent=+(100*c/a.max).toFixed(2)}),b.$on("$destroy",function(){c=null,d.removeBar(b)})},this.removeBar=function(a){this.bars.splice(this.bars.indexOf(a),1)}}]).directive("progress",function(){return{restrict:"EA",replace:!0,transclude:!0,controller:"ProgressController",require:"progress",scope:{},templateUrl:"template/progressbar/progress.html"}}).directive("bar",function(){return{restrict:"EA",replace:!0,transclude:!0,require:"^progress",scope:{value:"=",type:"@"},templateUrl:"template/progressbar/bar.html",link:function(a,b,c,d){d.addBar(a,b)}}}).directive("progressbar",function(){return{restrict:"EA",replace:!0,transclude:!0,controller:"ProgressController",scope:{value:"=",type:"@"},templateUrl:"template/progressbar/progressbar.html",link:function(a,b,c,d){d.addBar(a,angular.element(b.children()[0]))}}}),angular.module("ui.bootstrap.rating",[]).constant("ratingConfig",{max:5,stateOn:null,stateOff:null}).controller("RatingController",["$scope","$attrs","ratingConfig",function(a,b,c){var d={$setViewValue:angular.noop};this.init=function(e){d=e,d.$render=this.render,this.stateOn=angular.isDefined(b.stateOn)?a.$parent.$eval(b.stateOn):c.stateOn,this.stateOff=angular.isDefined(b.stateOff)?a.$parent.$eval(b.stateOff):c.stateOff;var f=angular.isDefined(b.ratingStates)?a.$parent.$eval(b.ratingStates):new Array(angular.isDefined(b.max)?a.$parent.$eval(b.max):c.max);a.range=this.buildTemplateObjects(f)},this.buildTemplateObjects=function(a){for(var b=0,c=a.length;c>b;b++)a[b]=angular.extend({index:b},{stateOn:this.stateOn,stateOff:this.stateOff},a[b]);return a},a.rate=function(b){!a.readonly&&b>=0&&b<=a.range.length&&(d.$setViewValue(b),d.$render())},a.enter=function(b){a.readonly||(a.value=b),a.onHover({value:b})},a.reset=function(){a.value=d.$viewValue,a.onLeave()},a.onKeydown=function(b){/(37|38|39|40)/.test(b.which)&&(b.preventDefault(),b.stopPropagation(),a.rate(a.value+(38===b.which||39===b.which?1:-1)))},this.render=function(){a.value=d.$viewValue}}]).directive("rating",function(){return{restrict:"EA",require:["rating","ngModel"],scope:{readonly:"=?",onHover:"&",onLeave:"&"},controller:"RatingController",templateUrl:"template/rating/rating.html",replace:!0,link:function(a,b,c,d){var e=d[0],f=d[1];f&&e.init(f)}}}),angular.module("ui.bootstrap.tabs",[]).controller("TabsetController",["$scope",function(a){var b=this,c=b.tabs=a.tabs=[];b.select=function(a){angular.forEach(c,function(b){b.active&&b!==a&&(b.active=!1,b.onDeselect())}),a.active=!0,a.onSelect()},b.addTab=function(a){c.push(a),1===c.length?a.active=!0:a.active&&b.select(a)},b.removeTab=function(a){var d=c.indexOf(a);if(a.active&&c.length>1){var e=d==c.length-1?d-1:d+1;b.select(c[e])}c.splice(d,1)}}]).directive("tabset",function(){return{restrict:"EA",transclude:!0,replace:!0,scope:{type:"@"},controller:"TabsetController",templateUrl:"template/tabs/tabset.html",link:function(a,b,c){a.vertical=angular.isDefined(c.vertical)?a.$parent.$eval(c.vertical):!1,a.justified=angular.isDefined(c.justified)?a.$parent.$eval(c.justified):!1}}}).directive("tab",["$parse",function(a){return{require:"^tabset",restrict:"EA",replace:!0,templateUrl:"template/tabs/tab.html",transclude:!0,scope:{active:"=?",heading:"@",onSelect:"&select",onDeselect:"&deselect"},controller:function(){},compile:function(b,c,d){return function(b,c,e,f){b.$watch("active",function(a){a&&f.select(b)}),b.disabled=!1,e.disabled&&b.$parent.$watch(a(e.disabled),function(a){b.disabled=!!a}),b.select=function(){b.disabled||(b.active=!0)},f.addTab(b),b.$on("$destroy",function(){f.removeTab(b)}),b.$transcludeFn=d}}}}]).directive("tabHeadingTransclude",[function(){return{restrict:"A",require:"^tab",link:function(a,b){a.$watch("headingElement",function(a){a&&(b.html(""),b.append(a))})}}}]).directive("tabContentTransclude",function(){function a(a){return a.tagName&&(a.hasAttribute("tab-heading")||a.hasAttribute("data-tab-heading")||"tab-heading"===a.tagName.toLowerCase()||"data-tab-heading"===a.tagName.toLowerCase())}return{restrict:"A",require:"^tabset",link:function(b,c,d){var e=b.$eval(d.tabContentTransclude);e.$transcludeFn(e.$parent,function(b){angular.forEach(b,function(b){a(b)?e.headingElement=b:c.append(b)})})}}}),angular.module("ui.bootstrap.timepicker",[]).constant("timepickerConfig",{hourStep:1,minuteStep:1,showMeridian:!0,meridians:null,readonlyInput:!1,mousewheel:!0}).controller("TimepickerController",["$scope","$attrs","$parse","$log","$locale","timepickerConfig",function(a,b,c,d,e,f){function g(){var b=parseInt(a.hours,10),c=a.showMeridian?b>0&&13>b:b>=0&&24>b;return c?(a.showMeridian&&(12===b&&(b=0),a.meridian===p[1]&&(b+=12)),b):void 0}function h(){var b=parseInt(a.minutes,10);return b>=0&&60>b?b:void 0}function i(a){return angular.isDefined(a)&&a.toString().length<2?"0"+a:a}function j(a){k(),o.$setViewValue(new Date(n)),l(a)}function k(){o.$setValidity("time",!0),a.invalidHours=!1,a.invalidMinutes=!1}function l(b){var c=n.getHours(),d=n.getMinutes();a.showMeridian&&(c=0===c||12===c?12:c%12),a.hours="h"===b?c:i(c),a.minutes="m"===b?d:i(d),a.meridian=n.getHours()<12?p[0]:p[1]}function m(a){var b=new Date(n.getTime()+6e4*a);n.setHours(b.getHours(),b.getMinutes()),j()}var n=new Date,o={$setViewValue:angular.noop},p=angular.isDefined(b.meridians)?a.$parent.$eval(b.meridians):f.meridians||e.DATETIME_FORMATS.AMPMS;this.init=function(c,d){o=c,o.$render=this.render;var e=d.eq(0),g=d.eq(1),h=angular.isDefined(b.mousewheel)?a.$parent.$eval(b.mousewheel):f.mousewheel;h&&this.setupMousewheelEvents(e,g),a.readonlyInput=angular.isDefined(b.readonlyInput)?a.$parent.$eval(b.readonlyInput):f.readonlyInput,this.setupInputEvents(e,g)};var q=f.hourStep;b.hourStep&&a.$parent.$watch(c(b.hourStep),function(a){q=parseInt(a,10)});var r=f.minuteStep;b.minuteStep&&a.$parent.$watch(c(b.minuteStep),function(a){r=parseInt(a,10)}),a.showMeridian=f.showMeridian,b.showMeridian&&a.$parent.$watch(c(b.showMeridian),function(b){if(a.showMeridian=!!b,o.$error.time){var c=g(),d=h();angular.isDefined(c)&&angular.isDefined(d)&&(n.setHours(c),j())}else l()}),this.setupMousewheelEvents=function(b,c){var d=function(a){a.originalEvent&&(a=a.originalEvent);var b=a.wheelDelta?a.wheelDelta:-a.deltaY;return a.detail||b>0};b.bind("mousewheel wheel",function(b){a.$apply(d(b)?a.incrementHours():a.decrementHours()),b.preventDefault()}),c.bind("mousewheel wheel",function(b){a.$apply(d(b)?a.incrementMinutes():a.decrementMinutes()),b.preventDefault()})},this.setupInputEvents=function(b,c){if(a.readonlyInput)return a.updateHours=angular.noop,void(a.updateMinutes=angular.noop);var d=function(b,c){o.$setViewValue(null),o.$setValidity("time",!1),angular.isDefined(b)&&(a.invalidHours=b),angular.isDefined(c)&&(a.invalidMinutes=c)};a.updateHours=function(){var a=g();angular.isDefined(a)?(n.setHours(a),j("h")):d(!0)},b.bind("blur",function(){!a.invalidHours&&a.hours<10&&a.$apply(function(){a.hours=i(a.hours)})}),a.updateMinutes=function(){var a=h();angular.isDefined(a)?(n.setMinutes(a),j("m")):d(void 0,!0)},c.bind("blur",function(){!a.invalidMinutes&&a.minutes<10&&a.$apply(function(){a.minutes=i(a.minutes)})})},this.render=function(){var a=o.$modelValue?new Date(o.$modelValue):null;isNaN(a)?(o.$setValidity("time",!1),d.error('Timepicker directive: "ng-model" value must be a Date object, a number of milliseconds since 01.01.1970 or a string representing an RFC2822 or ISO 8601 date.')):(a&&(n=a),k(),l())},a.incrementHours=function(){m(60*q)},a.decrementHours=function(){m(60*-q)},a.incrementMinutes=function(){m(r)},a.decrementMinutes=function(){m(-r)},a.toggleMeridian=function(){m(720*(n.getHours()<12?1:-1))}}]).directive("timepicker",function(){return{restrict:"EA",require:["timepicker","?^ngModel"],controller:"TimepickerController",replace:!0,scope:{},templateUrl:"template/timepicker/timepicker.html",link:function(a,b,c,d){var e=d[0],f=d[1];f&&e.init(f,b.find("input"))}}}),angular.module("ui.bootstrap.typeahead",["ui.bootstrap.position","ui.bootstrap.bindHtml"]).factory("typeaheadParser",["$parse",function(a){var b=/^\s*(.*?)(?:\s+as\s+(.*?))?\s+for\s+(?:([\$\w][\$\w\d]*))\s+in\s+(.*)$/;return{parse:function(c){var d=c.match(b);if(!d)throw new Error('Expected typeahead specification in form of "_modelValue_ (as _label_)? for _item_ in _collection_" but got "'+c+'".');return{itemName:d[3],source:a(d[4]),viewMapper:a(d[2]||d[1]),modelMapper:a(d[1])}}}}]).directive("typeahead",["$compile","$parse","$q","$timeout","$document","$position","typeaheadParser",function(a,b,c,d,e,f,g){var h=[9,13,27,38,40];return{require:"ngModel",link:function(i,j,k,l){var m,n=i.$eval(k.typeaheadMinLength)||1,o=i.$eval(k.typeaheadWaitMs)||0,p=i.$eval(k.typeaheadEditable)!==!1,q=b(k.typeaheadLoading).assign||angular.noop,r=b(k.typeaheadOnSelect),s=k.typeaheadInputFormatter?b(k.typeaheadInputFormatter):void 0,t=k.typeaheadAppendToBody?i.$eval(k.typeaheadAppendToBody):!1,u=b(k.ngModel).assign,v=g.parse(k.typeahead),w=i.$new();i.$on("$destroy",function(){w.$destroy()});var x="typeahead-"+w.$id+"-"+Math.floor(1e4*Math.random());j.attr({"aria-autocomplete":"list","aria-expanded":!1,"aria-owns":x});var y=angular.element("
        ");y.attr({id:x,matches:"matches",active:"activeIdx",select:"select(activeIdx)",query:"query",position:"position"}),angular.isDefined(k.typeaheadTemplateUrl)&&y.attr("template-url",k.typeaheadTemplateUrl);var z=function(){w.matches=[],w.activeIdx=-1,j.attr("aria-expanded",!1)},A=function(a){return x+"-option-"+a};w.$watch("activeIdx",function(a){0>a?j.removeAttr("aria-activedescendant"):j.attr("aria-activedescendant",A(a))});var B=function(a){var b={$viewValue:a};q(i,!0),c.when(v.source(i,b)).then(function(c){var d=a===l.$viewValue;if(d&&m)if(c.length>0){w.activeIdx=0,w.matches.length=0;for(var e=0;e=n?o>0?(C&&d.cancel(C),C=d(function(){B(a)},o)):B(a):(q(i,!1),z()),p?a:a?void l.$setValidity("editable",!1):(l.$setValidity("editable",!0),a)}),l.$formatters.push(function(a){var b,c,d={};return s?(d.$model=a,s(i,d)):(d[v.itemName]=a,b=v.viewMapper(i,d),d[v.itemName]=void 0,c=v.viewMapper(i,d),b!==c?b:a)}),w.select=function(a){var b,c,e={};e[v.itemName]=c=w.matches[a].model,b=v.modelMapper(i,e),u(i,b),l.$setValidity("editable",!0),r(i,{$item:c,$model:b,$label:v.viewMapper(i,e)}),z(),d(function(){j[0].focus()},0,!1)},j.bind("keydown",function(a){0!==w.matches.length&&-1!==h.indexOf(a.which)&&(a.preventDefault(),40===a.which?(w.activeIdx=(w.activeIdx+1)%w.matches.length,w.$digest()):38===a.which?(w.activeIdx=(w.activeIdx?w.activeIdx:w.matches.length)-1,w.$digest()):13===a.which||9===a.which?w.$apply(function(){w.select(w.activeIdx)}):27===a.which&&(a.stopPropagation(),z(),w.$digest()))}),j.bind("blur",function(){m=!1});var D=function(a){j[0]!==a.target&&(z(),w.$digest())};e.bind("click",D),i.$on("$destroy",function(){e.unbind("click",D)});var E=a(y)(w);t?e.find("body").append(E):j.after(E)}}}]).directive("typeaheadPopup",function(){return{restrict:"EA",scope:{matches:"=",query:"=",active:"=",position:"=",select:"&"},replace:!0,templateUrl:"template/typeahead/typeahead-popup.html",link:function(a,b,c){a.templateUrl=c.templateUrl,a.isOpen=function(){return a.matches.length>0},a.isActive=function(b){return a.active==b},a.selectActive=function(b){a.active=b},a.selectMatch=function(b){a.select({activeIdx:b})}}}}).directive("typeaheadMatch",["$http","$templateCache","$compile","$parse",function(a,b,c,d){return{restrict:"EA",scope:{index:"=",match:"=",query:"="},link:function(e,f,g){var h=d(g.templateUrl)(e.$parent)||"template/typeahead/typeahead-match.html";a.get(h,{cache:b}).success(function(a){f.replaceWith(c(a.trim())(e))})}}}]).filter("typeaheadHighlight",function(){function a(a){return a.replace(/([.?*+^$[\]\\(){}|-])/g,"\\$1")}return function(b,c){return c?(""+b).replace(new RegExp(a(c),"gi"),"$&"):b}}); \ No newline at end of file + */angular.module("ui.bootstrap",["ui.bootstrap.collapse","ui.bootstrap.accordion","ui.bootstrap.alert","ui.bootstrap.buttons","ui.bootstrap.carousel","ui.bootstrap.dateparser","ui.bootstrap.isClass","ui.bootstrap.datepicker","ui.bootstrap.position","ui.bootstrap.datepickerPopup","ui.bootstrap.debounce","ui.bootstrap.dropdown","ui.bootstrap.stackedMap","ui.bootstrap.modal","ui.bootstrap.paging","ui.bootstrap.pager","ui.bootstrap.pagination","ui.bootstrap.tooltip","ui.bootstrap.popover","ui.bootstrap.progressbar","ui.bootstrap.rating","ui.bootstrap.tabs","ui.bootstrap.timepicker","ui.bootstrap.typeahead"]),angular.module("ui.bootstrap.collapse",[]).directive("uibCollapse",["$animate","$q","$parse","$injector",function(a,b,c,d){var e=d.has("$animateCss")?d.get("$animateCss"):null;return{link:function(d,f,g){function h(){f.hasClass("collapse")&&f.hasClass("in")||b.resolve(l(d)).then(function(){f.removeClass("collapse").addClass("collapsing").attr("aria-expanded",!0).attr("aria-hidden",!1),e?e(f,{addClass:"in",easing:"ease",to:{height:f[0].scrollHeight+"px"}}).start()["finally"](i):a.addClass(f,"in",{to:{height:f[0].scrollHeight+"px"}}).then(i)})}function i(){f.removeClass("collapsing").addClass("collapse").css({height:"auto"}),m(d)}function j(){return f.hasClass("collapse")||f.hasClass("in")?void b.resolve(n(d)).then(function(){f.css({height:f[0].scrollHeight+"px"}).removeClass("collapse").addClass("collapsing").attr("aria-expanded",!1).attr("aria-hidden",!0),e?e(f,{removeClass:"in",to:{height:"0"}}).start()["finally"](k):a.removeClass(f,"in",{to:{height:"0"}}).then(k)}):k()}function k(){f.css({height:"0"}),f.removeClass("collapsing").addClass("collapse"),o(d)}var l=c(g.expanding),m=c(g.expanded),n=c(g.collapsing),o=c(g.collapsed);d.$eval(g.uibCollapse)||f.addClass("in").addClass("collapse").attr("aria-expanded",!0).attr("aria-hidden",!1).css({height:"auto"}),d.$watch(g.uibCollapse,function(a){a?j():h()})}}}]),angular.module("ui.bootstrap.accordion",["ui.bootstrap.collapse"]).constant("uibAccordionConfig",{closeOthers:!0}).controller("UibAccordionController",["$scope","$attrs","uibAccordionConfig",function(a,b,c){this.groups=[],this.closeOthers=function(d){var e=angular.isDefined(b.closeOthers)?a.$eval(b.closeOthers):c.closeOthers;e&&angular.forEach(this.groups,function(a){a!==d&&(a.isOpen=!1)})},this.addGroup=function(a){var b=this;this.groups.push(a),a.$on("$destroy",function(c){b.removeGroup(a)})},this.removeGroup=function(a){var b=this.groups.indexOf(a);-1!==b&&this.groups.splice(b,1)}}]).directive("uibAccordion",function(){return{controller:"UibAccordionController",controllerAs:"accordion",transclude:!0,templateUrl:function(a,b){return b.templateUrl||"uib/template/accordion/accordion.html"}}}).directive("uibAccordionGroup",function(){return{require:"^uibAccordion",transclude:!0,replace:!0,templateUrl:function(a,b){return b.templateUrl||"uib/template/accordion/accordion-group.html"},scope:{heading:"@",panelClass:"@?",isOpen:"=?",isDisabled:"=?"},controller:function(){this.setHeading=function(a){this.heading=a}},link:function(a,b,c,d){d.addGroup(a),a.openClass=c.openClass||"panel-open",a.panelClass=c.panelClass||"panel-default",a.$watch("isOpen",function(c){b.toggleClass(a.openClass,!!c),c&&d.closeOthers(a)}),a.toggleOpen=function(b){a.isDisabled||b&&32!==b.which||(a.isOpen=!a.isOpen)};var e="accordiongroup-"+a.$id+"-"+Math.floor(1e4*Math.random());a.headingId=e+"-tab",a.panelId=e+"-panel"}}}).directive("uibAccordionHeading",function(){return{transclude:!0,template:"",replace:!0,require:"^uibAccordionGroup",link:function(a,b,c,d,e){d.setHeading(e(a,angular.noop))}}}).directive("uibAccordionTransclude",function(){return{require:"^uibAccordionGroup",link:function(a,b,c,d){a.$watch(function(){return d[c.uibAccordionTransclude]},function(a){if(a){var c=angular.element(b[0].querySelector("[uib-accordion-header]"));c.html(""),c.append(a)}})}}}),angular.module("ui.bootstrap.alert",[]).controller("UibAlertController",["$scope","$attrs","$interpolate","$timeout",function(a,b,c,d){a.closeable=!!b.close;var e=angular.isDefined(b.dismissOnTimeout)?c(b.dismissOnTimeout)(a.$parent):null;e&&d(function(){a.close()},parseInt(e,10))}]).directive("uibAlert",function(){return{controller:"UibAlertController",controllerAs:"alert",templateUrl:function(a,b){return b.templateUrl||"uib/template/alert/alert.html"},transclude:!0,replace:!0,scope:{type:"@",close:"&"}}}),angular.module("ui.bootstrap.buttons",[]).constant("uibButtonConfig",{activeClass:"active",toggleEvent:"click"}).controller("UibButtonsController",["uibButtonConfig",function(a){this.activeClass=a.activeClass||"active",this.toggleEvent=a.toggleEvent||"click"}]).directive("uibBtnRadio",["$parse",function(a){return{require:["uibBtnRadio","ngModel"],controller:"UibButtonsController",controllerAs:"buttons",link:function(b,c,d,e){var f=e[0],g=e[1],h=a(d.uibUncheckable);c.find("input").css({display:"none"}),g.$render=function(){c.toggleClass(f.activeClass,angular.equals(g.$modelValue,b.$eval(d.uibBtnRadio)))},c.on(f.toggleEvent,function(){if(!d.disabled){var a=c.hasClass(f.activeClass);a&&!angular.isDefined(d.uncheckable)||b.$apply(function(){g.$setViewValue(a?null:b.$eval(d.uibBtnRadio)),g.$render()})}}),d.uibUncheckable&&b.$watch(h,function(a){d.$set("uncheckable",a?"":void 0)})}}}]).directive("uibBtnCheckbox",function(){return{require:["uibBtnCheckbox","ngModel"],controller:"UibButtonsController",controllerAs:"button",link:function(a,b,c,d){function e(){return g(c.btnCheckboxTrue,!0)}function f(){return g(c.btnCheckboxFalse,!1)}function g(b,c){return angular.isDefined(b)?a.$eval(b):c}var h=d[0],i=d[1];b.find("input").css({display:"none"}),i.$render=function(){b.toggleClass(h.activeClass,angular.equals(i.$modelValue,e()))},b.on(h.toggleEvent,function(){c.disabled||a.$apply(function(){i.$setViewValue(b.hasClass(h.activeClass)?f():e()),i.$render()})})}}}),angular.module("ui.bootstrap.carousel",[]).controller("UibCarouselController",["$scope","$element","$interval","$timeout","$animate",function(a,b,c,d,e){function f(){for(;t.length;)t.shift()}function g(a){for(var b=0;b1){q[d].element.data(r,c.direction);var j=p.getCurrentIndex();angular.isNumber(j)&&q[j].element&&q[j].element.data(r,c.direction),a.$currentTransition=!0,e.on("addClass",q[d].element,function(b,c){if("close"===c&&(a.$currentTransition=null,e.off("addClass",b),t.length)){var d=t.pop().slide,g=d.index,i=g>p.getCurrentIndex()?"next":"prev";f(),h(d,g,i)}})}a.active=c.index,s=c.index,g(d),l()}}function i(a){for(var b=0;b0&&(n=c(m,b))}function m(){var b=+a.interval;o&&!isNaN(b)&&b>0&&q.length?a.next():a.pause()}var n,o,p=this,q=p.slides=a.slides=[],r="uib-slideDirection",s=a.active,t=[],u=!1;p.addSlide=function(b,c){q.push({slide:b,element:c}),q.sort(function(a,b){return+a.slide.index-+b.slide.index}),(b.index===a.active||1===q.length&&!angular.isNumber(a.active))&&(a.$currentTransition&&(a.$currentTransition=null),s=b.index,a.active=b.index,g(s),p.select(q[i(b)]),1===q.length&&a.play())},p.getCurrentIndex=function(){for(var a=0;a0&&s===c?c>=q.length?(s=q.length-1,a.active=s,g(s),p.select(q[q.length-1])):(s=c,a.active=s,g(s),p.select(q[c])):s>c&&(s--,a.active=s),0===q.length&&(s=null,a.active=null,f())},p.select=a.select=function(b,c){var d=i(b.slide);void 0===c&&(c=d>p.getCurrentIndex()?"next":"prev"),b.slide.index===s||a.$currentTransition?b&&b.slide.index!==s&&a.$currentTransition&&t.push(q[d]):h(b.slide,d,c)},a.indexOfSlide=function(a){return+a.slide.index},a.isActive=function(b){return a.active===b.slide.index},a.isPrevDisabled=function(){return 0===a.active&&a.noWrap()},a.isNextDisabled=function(){return a.active===q.length-1&&a.noWrap()},a.pause=function(){a.noPause||(o=!1,j())},a.play=function(){o||(o=!0,l())},a.$on("$destroy",function(){u=!0,j()}),a.$watch("noTransition",function(a){e.enabled(b,!a)}),a.$watch("interval",l),a.$watchCollection("slides",k),a.$watch("active",function(a){if(angular.isNumber(a)&&s!==a){for(var b=0;b-1){var g=!1;a=a.split("");for(var h=f;h-1){a=a.split(""),e[f]="("+d.regex+")",a[f]="$";for(var g=f+1,h=f+d.key.length;h>g;g++)e[g]="",a[g]="$";a=a.join(""),c.push({index:f,key:d.key,apply:d[b],matcher:d.regex})}}),{regex:new RegExp("^"+e.join("")+"$"),map:d(c,"index")}}function f(a,b,c){return 1>c?!1:1===b&&c>28?29===c&&(a%4===0&&a%100!==0||a%400===0):3===b||5===b||8===b||10===b?31>c:!0}function g(a){return parseInt(a,10)}function h(a,b){return a&&b?l(a,b):a}function i(a,b){return a&&b?l(a,b,!0):a}function j(a,b){var c=Date.parse("Jan 01, 1970 00:00:00 "+a)/6e4;return isNaN(c)?b:c}function k(a,b){return a=new Date(a.getTime()),a.setMinutes(a.getMinutes()+b),a}function l(a,b,c){c=c?-1:1;var d=j(b,a.getTimezoneOffset());return k(a,c*(d-a.getTimezoneOffset()))}var m,n,o=/[\\\^\$\*\+\?\|\[\]\(\)\.\{\}]/g;this.init=function(){m=b.id,this.parsers={},this.formatters={},n=[{key:"yyyy",regex:"\\d{4}",apply:function(a){this.year=+a},formatter:function(a){var b=new Date;return b.setFullYear(Math.abs(a.getFullYear())),c(b,"yyyy")}},{key:"yy",regex:"\\d{2}",apply:function(a){a=+a,this.year=69>a?a+2e3:a+1900},formatter:function(a){var b=new Date;return b.setFullYear(Math.abs(a.getFullYear())),c(b,"yy")}},{key:"y",regex:"\\d{1,4}",apply:function(a){this.year=+a},formatter:function(a){var b=new Date;return b.setFullYear(Math.abs(a.getFullYear())),c(b,"y")}},{key:"M!",regex:"0?[1-9]|1[0-2]",apply:function(a){this.month=a-1},formatter:function(a){var b=a.getMonth();return/^[0-9]$/.test(b)?c(a,"MM"):c(a,"M")}},{key:"MMMM",regex:b.DATETIME_FORMATS.MONTH.join("|"),apply:function(a){this.month=b.DATETIME_FORMATS.MONTH.indexOf(a)},formatter:function(a){return c(a,"MMMM")}},{key:"MMM",regex:b.DATETIME_FORMATS.SHORTMONTH.join("|"),apply:function(a){this.month=b.DATETIME_FORMATS.SHORTMONTH.indexOf(a)},formatter:function(a){return c(a,"MMM")}},{key:"MM",regex:"0[1-9]|1[0-2]",apply:function(a){this.month=a-1},formatter:function(a){return c(a,"MM")}},{key:"M",regex:"[1-9]|1[0-2]",apply:function(a){this.month=a-1},formatter:function(a){return c(a,"M")}},{key:"d!",regex:"[0-2]?[0-9]{1}|3[0-1]{1}",apply:function(a){this.date=+a},formatter:function(a){var b=a.getDate();return/^[1-9]$/.test(b)?c(a,"dd"):c(a,"d")}},{key:"dd",regex:"[0-2][0-9]{1}|3[0-1]{1}",apply:function(a){this.date=+a},formatter:function(a){return c(a,"dd")}},{key:"d",regex:"[1-2]?[0-9]{1}|3[0-1]{1}",apply:function(a){this.date=+a},formatter:function(a){return c(a,"d")}},{key:"EEEE",regex:b.DATETIME_FORMATS.DAY.join("|"),formatter:function(a){return c(a,"EEEE")}},{key:"EEE",regex:b.DATETIME_FORMATS.SHORTDAY.join("|"),formatter:function(a){return c(a,"EEE")}},{key:"HH",regex:"(?:0|1)[0-9]|2[0-3]",apply:function(a){this.hours=+a},formatter:function(a){return c(a,"HH")}},{key:"hh",regex:"0[0-9]|1[0-2]",apply:function(a){this.hours=+a},formatter:function(a){return c(a,"hh")}},{key:"H",regex:"1?[0-9]|2[0-3]",apply:function(a){this.hours=+a},formatter:function(a){return c(a,"H")}},{key:"h",regex:"[0-9]|1[0-2]",apply:function(a){this.hours=+a},formatter:function(a){return c(a,"h")}},{key:"mm",regex:"[0-5][0-9]",apply:function(a){this.minutes=+a},formatter:function(a){return c(a,"mm")}},{key:"m",regex:"[0-9]|[1-5][0-9]",apply:function(a){this.minutes=+a},formatter:function(a){return c(a,"m")}},{key:"sss",regex:"[0-9][0-9][0-9]",apply:function(a){this.milliseconds=+a},formatter:function(a){return c(a,"sss")}},{key:"ss",regex:"[0-5][0-9]",apply:function(a){this.seconds=+a},formatter:function(a){return c(a,"ss")}},{key:"s",regex:"[0-9]|[1-5][0-9]",apply:function(a){this.seconds=+a},formatter:function(a){return c(a,"s")}},{key:"a",regex:b.DATETIME_FORMATS.AMPMS.join("|"),apply:function(a){12===this.hours&&(this.hours=0),"PM"===a&&(this.hours+=12)},formatter:function(a){return c(a,"a")}},{key:"Z",regex:"[+-]\\d{4}",apply:function(a){var b=a.match(/([+-])(\d{2})(\d{2})/),c=b[1],d=b[2],e=b[3];this.hours+=g(c+d),this.minutes+=g(c+e)},formatter:function(a){return c(a,"Z")}},{key:"ww",regex:"[0-4][0-9]|5[0-3]",formatter:function(a){return c(a,"ww")}},{key:"w",regex:"[0-9]|[1-4][0-9]|5[0-3]",formatter:function(a){return c(a,"w")}},{key:"GGGG",regex:b.DATETIME_FORMATS.ERANAMES.join("|").replace(/\s/g,"\\s"),formatter:function(a){return c(a,"GGGG")}},{key:"GGG",regex:b.DATETIME_FORMATS.ERAS.join("|"),formatter:function(a){return c(a,"GGG")}},{key:"GG",regex:b.DATETIME_FORMATS.ERAS.join("|"),formatter:function(a){return c(a,"GG")}},{key:"G",regex:b.DATETIME_FORMATS.ERAS.join("|"),formatter:function(a){return c(a,"G")}}]},this.init(),this.filter=function(a,c){if(!angular.isDate(a)||isNaN(a)||!c)return"";c=b.DATETIME_FORMATS[c]||c,b.id!==m&&this.init(),this.formatters[c]||(this.formatters[c]=e(c,"formatter"));var d=this.formatters[c],f=d.map,g=c;return f.reduce(function(b,c,d){var e=g.match(new RegExp("(.*)"+c.key));e&&angular.isString(e[1])&&(b+=e[1],g=g.replace(e[1]+c.key,""));var h=d===f.length-1?g:"";return c.apply?b+c.apply.call(null,a)+h:b+h},"")},this.parse=function(c,d,g){if(!angular.isString(c)||!d)return c;d=b.DATETIME_FORMATS[d]||d,d=d.replace(o,"\\$&"),b.id!==m&&this.init(),this.parsers[d]||(this.parsers[d]=e(d,"apply"));var h=this.parsers[d],i=h.regex,j=h.map,k=c.match(i),l=!1;if(k&&k.length){var n,p;angular.isDate(g)&&!isNaN(g.getTime())?n={year:g.getFullYear(),month:g.getMonth(),date:g.getDate(),hours:g.getHours(),minutes:g.getMinutes(),seconds:g.getSeconds(),milliseconds:g.getMilliseconds()}:(g&&a.warn("dateparser:","baseDate is not a valid date"),n={year:1900,month:0,date:1,hours:0,minutes:0,seconds:0,milliseconds:0});for(var q=1,r=k.length;r>q;q++){var s=j[q-1];"Z"===s.matcher&&(l=!0),s.apply&&s.apply.call(n,k[q])}var t=l?Date.prototype.setUTCFullYear:Date.prototype.setFullYear,u=l?Date.prototype.setUTCHours:Date.prototype.setHours;return f(n.year,n.month,n.date)&&(!angular.isDate(g)||isNaN(g.getTime())||l?(p=new Date(0),t.call(p,n.year,n.month,n.date),u.call(p,n.hours||0,n.minutes||0,n.seconds||0,n.milliseconds||0)):(p=new Date(g),t.call(p,n.year,n.month,n.date),u.call(p,n.hours,n.minutes,n.seconds,n.milliseconds))),p}},this.toTimezone=h,this.fromTimezone=i,this.timezoneToOffset=j,this.addDateMinutes=k,this.convertTimezoneToLocal=l}]),angular.module("ui.bootstrap.isClass",[]).directive("uibIsClass",["$animate",function(a){var b=/^\s*([\s\S]+?)\s+on\s+([\s\S]+?)\s*$/,c=/^\s*([\s\S]+?)\s+for\s+([\s\S]+?)\s*$/;return{restrict:"A",compile:function(d,e){function f(a,b,c){i.push(a),j.push({scope:a,element:b}),o.forEach(function(b,c){g(b,a)}),a.$on("$destroy",h)}function g(b,d){var e=b.match(c),f=d.$eval(e[1]),g=e[2],h=k[b];if(!h){var i=function(b){var c=null;j.some(function(a){var d=a.scope.$eval(m);return d===b?(c=a,!0):void 0}),h.lastActivated!==c&&(h.lastActivated&&a.removeClass(h.lastActivated.element,f),c&&a.addClass(c.element,f),h.lastActivated=c)};k[b]=h={lastActivated:null,scope:d,watchFn:i,compareWithExp:g,watcher:d.$watch(g,i)}}h.watchFn(d.$eval(g))}function h(a){var b=a.targetScope,c=i.indexOf(b);if(i.splice(c,1),j.splice(c,1),i.length){var d=i[0];angular.forEach(k,function(a){a.scope===b&&(a.watcher=d.$watch(a.compareWithExp,a.watchFn),a.scope=d)})}else k={}}var i=[],j=[],k={},l=e.uibIsClass.match(b),m=l[2],n=l[1],o=n.split(",");return f}}}]),angular.module("ui.bootstrap.datepicker",["ui.bootstrap.dateparser","ui.bootstrap.isClass"]).value("$datepickerSuppressError",!1).value("$datepickerLiteralWarning",!0).constant("uibDatepickerConfig",{datepickerMode:"day",formatDay:"dd",formatMonth:"MMMM",formatYear:"yyyy",formatDayHeader:"EEE",formatDayTitle:"MMMM yyyy",formatMonthTitle:"yyyy",maxDate:null,maxMode:"year",minDate:null,minMode:"day",ngModelOptions:{},shortcutPropagation:!1,showWeeks:!0,yearColumns:5,yearRows:4}).controller("UibDatepickerController",["$scope","$attrs","$parse","$interpolate","$locale","$log","dateFilter","uibDatepickerConfig","$datepickerLiteralWarning","$datepickerSuppressError","uibDateParser",function(a,b,c,d,e,f,g,h,i,j,k){function l(b){a.datepickerMode=b,a.datepickerOptions.datepickerMode=b}var m=this,n={$setViewValue:angular.noop},o={},p=[];!!b.datepickerOptions;a.datepickerOptions||(a.datepickerOptions={}),this.modes=["day","month","year"],["customClass","dateDisabled","datepickerMode","formatDay","formatDayHeader","formatDayTitle","formatMonth","formatMonthTitle","formatYear","maxDate","maxMode","minDate","minMode","showWeeks","shortcutPropagation","startingDay","yearColumns","yearRows"].forEach(function(b){switch(b){case"customClass":case"dateDisabled":a[b]=a.datepickerOptions[b]||angular.noop;break;case"datepickerMode":a.datepickerMode=angular.isDefined(a.datepickerOptions.datepickerMode)?a.datepickerOptions.datepickerMode:h.datepickerMode;break;case"formatDay":case"formatDayHeader":case"formatDayTitle":case"formatMonth":case"formatMonthTitle":case"formatYear":m[b]=angular.isDefined(a.datepickerOptions[b])?d(a.datepickerOptions[b])(a.$parent):h[b];break;case"showWeeks":case"shortcutPropagation":case"yearColumns":case"yearRows":m[b]=angular.isDefined(a.datepickerOptions[b])?a.datepickerOptions[b]:h[b];break;case"startingDay":angular.isDefined(a.datepickerOptions.startingDay)?m.startingDay=a.datepickerOptions.startingDay:angular.isNumber(h.startingDay)?m.startingDay=h.startingDay:m.startingDay=(e.DATETIME_FORMATS.FIRSTDAYOFWEEK+8)%7;break;case"maxDate":case"minDate":a.$watch("datepickerOptions."+b,function(a){a?angular.isDate(a)?m[b]=k.fromTimezone(new Date(a),o.timezone):(i&&f.warn("Literal date support has been deprecated, please switch to date object usage"),m[b]=new Date(g(a,"medium"))):m[b]=h[b]?k.fromTimezone(new Date(h[b]),o.timezone):null,m.refreshView()});break;case"maxMode":case"minMode":a.datepickerOptions[b]?a.$watch(function(){return a.datepickerOptions[b]},function(c){m[b]=a[b]=angular.isDefined(c)?c:datepickerOptions[b],("minMode"===b&&m.modes.indexOf(a.datepickerOptions.datepickerMode)m.modes.indexOf(m[b]))&&(a.datepickerMode=m[b],a.datepickerOptions.datepickerMode=m[b])}):m[b]=a[b]=h[b]||null}}),a.uniqueId="datepicker-"+a.$id+"-"+Math.floor(1e4*Math.random()),a.disabled=angular.isDefined(b.disabled)||!1,angular.isDefined(b.ngDisabled)&&p.push(a.$parent.$watch(b.ngDisabled,function(b){a.disabled=b,m.refreshView()})),a.isActive=function(b){return 0===m.compare(b.date,m.activeDate)?(a.activeDateId=b.uid,!0):!1},this.init=function(b){n=b,o=b.$options||h.ngModelOptions,a.datepickerOptions.initDate?(m.activeDate=k.fromTimezone(a.datepickerOptions.initDate,o.timezone)||new Date,a.$watch("datepickerOptions.initDate",function(a){a&&(n.$isEmpty(n.$modelValue)||n.$invalid)&&(m.activeDate=k.fromTimezone(a,o.timezone),m.refreshView())})):m.activeDate=new Date,this.activeDate=n.$modelValue?k.fromTimezone(new Date(n.$modelValue),o.timezone):k.fromTimezone(new Date,o.timezone),n.$render=function(){m.render()}},this.render=function(){if(n.$viewValue){var a=new Date(n.$viewValue),b=!isNaN(a);b?this.activeDate=k.fromTimezone(a,o.timezone):j||f.error('Datepicker directive: "ng-model" value must be a Date object')}this.refreshView()},this.refreshView=function(){if(this.element){a.selectedDt=null,this._refreshView(),a.activeDt&&(a.activeDateId=a.activeDt.uid);var b=n.$viewValue?new Date(n.$viewValue):null;b=k.fromTimezone(b,o.timezone),n.$setValidity("dateDisabled",!b||this.element&&!this.isDisabled(b))}},this.createDateObject=function(b,c){var d=n.$viewValue?new Date(n.$viewValue):null;d=k.fromTimezone(d,o.timezone);var e=new Date;e=k.fromTimezone(e,o.timezone);var f=this.compare(b,e),g={date:b,label:k.filter(b,c),selected:d&&0===this.compare(b,d),disabled:this.isDisabled(b),past:0>f,current:0===f,future:f>0,customClass:this.customClass(b)||null};return d&&0===this.compare(b,d)&&(a.selectedDt=g),m.activeDate&&0===this.compare(g.date,m.activeDate)&&(a.activeDt=g),g},this.isDisabled=function(b){return a.disabled||this.minDate&&this.compare(b,this.minDate)<0||this.maxDate&&this.compare(b,this.maxDate)>0||a.dateDisabled&&a.dateDisabled({date:b,mode:a.datepickerMode})},this.customClass=function(b){return a.customClass({date:b,mode:a.datepickerMode})},this.split=function(a,b){for(var c=[];a.length>0;)c.push(a.splice(0,b));return c},a.select=function(b){if(a.datepickerMode===m.minMode){var c=n.$viewValue?k.fromTimezone(new Date(n.$viewValue),o.timezone):new Date(0,0,0,0,0,0,0);c.setFullYear(b.getFullYear(),b.getMonth(),b.getDate()),c=k.toTimezone(c,o.timezone),n.$setViewValue(c),n.$render()}else m.activeDate=b,l(m.modes[m.modes.indexOf(a.datepickerMode)-1]),a.$emit("uib:datepicker.mode");a.$broadcast("uib:datepicker.focus")},a.move=function(a){var b=m.activeDate.getFullYear()+a*(m.step.years||0),c=m.activeDate.getMonth()+a*(m.step.months||0);m.activeDate.setFullYear(b,c,1),m.refreshView()},a.toggleMode=function(b){b=b||1,a.datepickerMode===m.maxMode&&1===b||a.datepickerMode===m.minMode&&-1===b||(l(m.modes[m.modes.indexOf(a.datepickerMode)+b]),a.$emit("uib:datepicker.mode"))},a.keys={13:"enter",32:"space",33:"pageup",34:"pagedown",35:"end",36:"home",37:"left",38:"up",39:"right",40:"down"};var q=function(){m.element[0].focus()};a.$on("uib:datepicker.focus",q),a.keydown=function(b){var c=a.keys[b.which];if(c&&!b.shiftKey&&!b.altKey&&!a.disabled)if(b.preventDefault(),m.shortcutPropagation||b.stopPropagation(),"enter"===c||"space"===c){if(m.isDisabled(m.activeDate))return;a.select(m.activeDate)}else!b.ctrlKey||"up"!==c&&"down"!==c?(m.handleKeyDown(c,b),m.refreshView()):a.toggleMode("up"===c?1:-1)},a.$on("$destroy",function(){for(;p.length;)p.shift()()})}]).controller("UibDaypickerController",["$scope","$element","dateFilter",function(a,b,c){function d(a,b){return 1!==b||a%4!==0||a%100===0&&a%400!==0?f[b]:29}function e(a){var b=new Date(a);b.setDate(b.getDate()+4-(b.getDay()||7));var c=b.getTime();return b.setMonth(0),b.setDate(1),Math.floor(Math.round((c-b)/864e5)/7)+1}var f=[31,28,31,30,31,30,31,31,30,31,30,31];this.step={months:1},this.element=b,this.init=function(b){angular.extend(b,this),a.showWeeks=b.showWeeks,b.refreshView()},this.getDates=function(a,b){for(var c,d=new Array(b),e=new Date(a),f=0;b>f;)c=new Date(e),d[f++]=c,e.setDate(e.getDate()+1);return d},this._refreshView=function(){var b=this.activeDate.getFullYear(),d=this.activeDate.getMonth(),f=new Date(this.activeDate);f.setFullYear(b,d,1);var g=this.startingDay-f.getDay(),h=g>0?7-g:-g,i=new Date(f);h>0&&i.setDate(-h+1);for(var j=this.getDates(i,42),k=0;42>k;k++)j[k]=angular.extend(this.createDateObject(j[k],this.formatDay),{secondary:j[k].getMonth()!==d,uid:a.uniqueId+"-"+k});a.labels=new Array(7);for(var l=0;7>l;l++)a.labels[l]={abbr:c(j[l].date,this.formatDayHeader),full:c(j[l].date,"EEEE")};if(a.title=c(this.activeDate,this.formatDayTitle),a.rows=this.split(j,7),a.showWeeks){a.weekNumbers=[];for(var m=(11-this.startingDay)%7,n=a.rows.length,o=0;n>o;o++)a.weekNumbers.push(e(a.rows[o][m].date))}},this.compare=function(a,b){var c=new Date(a.getFullYear(),a.getMonth(),a.getDate()),d=new Date(b.getFullYear(),b.getMonth(),b.getDate());return c.setFullYear(a.getFullYear()),d.setFullYear(b.getFullYear()),c-d},this.handleKeyDown=function(a,b){var c=this.activeDate.getDate();if("left"===a)c-=1;else if("up"===a)c-=7;else if("right"===a)c+=1;else if("down"===a)c+=7;else if("pageup"===a||"pagedown"===a){var e=this.activeDate.getMonth()+("pageup"===a?-1:1);this.activeDate.setMonth(e,1),c=Math.min(d(this.activeDate.getFullYear(),this.activeDate.getMonth()),c)}else"home"===a?c=1:"end"===a&&(c=d(this.activeDate.getFullYear(),this.activeDate.getMonth()));this.activeDate.setDate(c)}}]).controller("UibMonthpickerController",["$scope","$element","dateFilter",function(a,b,c){this.step={years:1},this.element=b,this.init=function(a){angular.extend(a,this),a.refreshView()},this._refreshView=function(){for(var b,d=new Array(12),e=this.activeDate.getFullYear(),f=0;12>f;f++)b=new Date(this.activeDate),b.setFullYear(e,f,1),d[f]=angular.extend(this.createDateObject(b,this.formatMonth),{uid:a.uniqueId+"-"+f});a.title=c(this.activeDate,this.formatMonthTitle),a.rows=this.split(d,3)},this.compare=function(a,b){var c=new Date(a.getFullYear(),a.getMonth()),d=new Date(b.getFullYear(),b.getMonth());return c.setFullYear(a.getFullYear()),d.setFullYear(b.getFullYear()),c-d},this.handleKeyDown=function(a,b){var c=this.activeDate.getMonth();if("left"===a)c-=1;else if("up"===a)c-=3;else if("right"===a)c+=1;else if("down"===a)c+=3;else if("pageup"===a||"pagedown"===a){var d=this.activeDate.getFullYear()+("pageup"===a?-1:1);this.activeDate.setFullYear(d)}else"home"===a?c=0:"end"===a&&(c=11);this.activeDate.setMonth(c)}}]).controller("UibYearpickerController",["$scope","$element","dateFilter",function(a,b,c){function d(a){return parseInt((a-1)/f,10)*f+1}var e,f;this.element=b,this.yearpickerInit=function(){e=this.yearColumns,f=this.yearRows*e,this.step={years:f}},this._refreshView=function(){for(var b,c=new Array(f),g=0,h=d(this.activeDate.getFullYear());f>g;g++)b=new Date(this.activeDate),b.setFullYear(h+g,0,1),c[g]=angular.extend(this.createDateObject(b,this.formatYear),{uid:a.uniqueId+"-"+g});a.title=[c[0].label,c[f-1].label].join(" - "),a.rows=this.split(c,e),a.columns=e},this.compare=function(a,b){return a.getFullYear()-b.getFullYear()},this.handleKeyDown=function(a,b){var c=this.activeDate.getFullYear();"left"===a?c-=1:"up"===a?c-=e:"right"===a?c+=1:"down"===a?c+=e:"pageup"===a||"pagedown"===a?c+=("pageup"===a?-1:1)*f:"home"===a?c=d(this.activeDate.getFullYear()):"end"===a&&(c=d(this.activeDate.getFullYear())+f-1),this.activeDate.setFullYear(c)}}]).directive("uibDatepicker",function(){return{replace:!0,templateUrl:function(a,b){return b.templateUrl||"uib/template/datepicker/datepicker.html"},scope:{datepickerOptions:"=?"},require:["uibDatepicker","^ngModel"],controller:"UibDatepickerController",controllerAs:"datepicker",link:function(a,b,c,d){var e=d[0],f=d[1];e.init(f)}}}).directive("uibDaypicker",function(){return{replace:!0,templateUrl:function(a,b){return b.templateUrl||"uib/template/datepicker/day.html"},require:["^uibDatepicker","uibDaypicker"],controller:"UibDaypickerController",link:function(a,b,c,d){var e=d[0],f=d[1];f.init(e)}}}).directive("uibMonthpicker",function(){return{replace:!0,templateUrl:function(a,b){return b.templateUrl||"uib/template/datepicker/month.html"},require:["^uibDatepicker","uibMonthpicker"],controller:"UibMonthpickerController",link:function(a,b,c,d){var e=d[0],f=d[1];f.init(e)}}}).directive("uibYearpicker",function(){return{replace:!0,templateUrl:function(a,b){return b.templateUrl||"uib/template/datepicker/year.html"},require:["^uibDatepicker","uibYearpicker"],controller:"UibYearpickerController",link:function(a,b,c,d){var e=d[0];angular.extend(e,d[1]),e.yearpickerInit(),e.refreshView()}}}),angular.module("ui.bootstrap.position",[]).factory("$uibPosition",["$document","$window",function(a,b){var c,d,e={normal:/(auto|scroll)/,hidden:/(auto|scroll|hidden)/},f={auto:/\s?auto?\s?/i,primary:/^(top|bottom|left|right)$/,secondary:/^(top|bottom|left|right|center)$/,vertical:/^(top|bottom)$/},g=/(HTML|BODY)/;return{getRawNode:function(a){return a.nodeName?a:a[0]||a},parseStyle:function(a){return a=parseFloat(a),isFinite(a)?a:0},offsetParent:function(c){function d(a){return"static"===(b.getComputedStyle(a).position||"static")}c=this.getRawNode(c);for(var e=c.offsetParent||a[0].documentElement;e&&e!==a[0].documentElement&&d(e);)e=e.offsetParent;return e||a[0].documentElement},scrollbarWidth:function(e){if(e){if(angular.isUndefined(d)){var f=a.find("body");f.addClass("uib-position-body-scrollbar-measure"),d=b.innerWidth-f[0].clientWidth,d=isFinite(d)?d:0,f.removeClass("uib-position-body-scrollbar-measure")}return d}if(angular.isUndefined(c)){var g=angular.element('
        ');a.find("body").append(g),c=g[0].offsetWidth-g[0].clientWidth,c=isFinite(c)?c:0,g.remove()}return c},scrollbarPadding:function(a){a=this.getRawNode(a);var c=b.getComputedStyle(a),d=this.parseStyle(c.paddingRight),e=this.parseStyle(c.paddingBottom),f=this.scrollParent(a,!1,!0),h=this.scrollbarWidth(f,g.test(f.tagName));return{scrollbarWidth:h,widthOverflow:f.scrollWidth>f.clientWidth,right:d+h,originalRight:d,heightOverflow:f.scrollHeight>f.clientHeight,bottom:e+h,originalBottom:e}},isScrollable:function(a,c){a=this.getRawNode(a);var d=c?e.hidden:e.normal,f=b.getComputedStyle(a);return d.test(f.overflow+f.overflowY+f.overflowX)},scrollParent:function(c,d,f){c=this.getRawNode(c);var g=d?e.hidden:e.normal,h=a[0].documentElement,i=b.getComputedStyle(c);if(f&&g.test(i.overflow+i.overflowY+i.overflowX))return c;var j="absolute"===i.position,k=c.parentElement||h;if(k===h||"fixed"===i.position)return h;for(;k.parentElement&&k!==h;){var l=b.getComputedStyle(k);if(j&&"static"!==l.position&&(j=!1),!j&&g.test(l.overflow+l.overflowY+l.overflowX))break;k=k.parentElement}return k},position:function(c,d){c=this.getRawNode(c);var e=this.offset(c);if(d){var f=b.getComputedStyle(c);e.top-=this.parseStyle(f.marginTop),e.left-=this.parseStyle(f.marginLeft)}var g=this.offsetParent(c),h={top:0,left:0};return g!==a[0].documentElement&&(h=this.offset(g),h.top+=g.clientTop-g.scrollTop,h.left+=g.clientLeft-g.scrollLeft),{width:Math.round(angular.isNumber(e.width)?e.width:c.offsetWidth),height:Math.round(angular.isNumber(e.height)?e.height:c.offsetHeight),top:Math.round(e.top-h.top),left:Math.round(e.left-h.left)}},offset:function(c){c=this.getRawNode(c);var d=c.getBoundingClientRect();return{width:Math.round(angular.isNumber(d.width)?d.width:c.offsetWidth),height:Math.round(angular.isNumber(d.height)?d.height:c.offsetHeight), +top:Math.round(d.top+(b.pageYOffset||a[0].documentElement.scrollTop)),left:Math.round(d.left+(b.pageXOffset||a[0].documentElement.scrollLeft))}},viewportOffset:function(c,d,e){c=this.getRawNode(c),e=e!==!1;var f=c.getBoundingClientRect(),g={top:0,left:0,bottom:0,right:0},h=d?a[0].documentElement:this.scrollParent(c),i=h.getBoundingClientRect();if(g.top=i.top+h.clientTop,g.left=i.left+h.clientLeft,h===a[0].documentElement&&(g.top+=b.pageYOffset,g.left+=b.pageXOffset),g.bottom=g.top+h.clientHeight,g.right=g.left+h.clientWidth,e){var j=b.getComputedStyle(h);g.top+=this.parseStyle(j.paddingTop),g.bottom-=this.parseStyle(j.paddingBottom),g.left+=this.parseStyle(j.paddingLeft),g.right-=this.parseStyle(j.paddingRight)}return{top:Math.round(f.top-g.top),bottom:Math.round(g.bottom-f.bottom),left:Math.round(f.left-g.left),right:Math.round(g.right-f.right)}},parsePlacement:function(a){var b=f.auto.test(a);return b&&(a=a.replace(f.auto,"")),a=a.split("-"),a[0]=a[0]||"top",f.primary.test(a[0])||(a[0]="top"),a[1]=a[1]||"center",f.secondary.test(a[1])||(a[1]="center"),b?a[2]=!0:a[2]=!1,a},positionElements:function(a,c,d,e){a=this.getRawNode(a),c=this.getRawNode(c);var g=angular.isDefined(c.offsetWidth)?c.offsetWidth:c.prop("offsetWidth"),h=angular.isDefined(c.offsetHeight)?c.offsetHeight:c.prop("offsetHeight");d=this.parsePlacement(d);var i=e?this.offset(a):this.position(a),j={top:0,left:0,placement:""};if(d[2]){var k=this.viewportOffset(a,e),l=b.getComputedStyle(c),m={width:g+Math.round(Math.abs(this.parseStyle(l.marginLeft)+this.parseStyle(l.marginRight))),height:h+Math.round(Math.abs(this.parseStyle(l.marginTop)+this.parseStyle(l.marginBottom)))};if(d[0]="top"===d[0]&&m.height>k.top&&m.height<=k.bottom?"bottom":"bottom"===d[0]&&m.height>k.bottom&&m.height<=k.top?"top":"left"===d[0]&&m.width>k.left&&m.width<=k.right?"right":"right"===d[0]&&m.width>k.right&&m.width<=k.left?"left":d[0],d[1]="top"===d[1]&&m.height-i.height>k.bottom&&m.height-i.height<=k.top?"bottom":"bottom"===d[1]&&m.height-i.height>k.top&&m.height-i.height<=k.bottom?"top":"left"===d[1]&&m.width-i.width>k.right&&m.width-i.width<=k.left?"right":"right"===d[1]&&m.width-i.width>k.left&&m.width-i.width<=k.right?"left":d[1],"center"===d[1])if(f.vertical.test(d[0])){var n=i.width/2-g/2;k.left+n<0&&m.width-i.width<=k.right?d[1]="left":k.right+n<0&&m.width-i.width<=k.left&&(d[1]="right")}else{var o=i.height/2-m.height/2;k.top+o<0&&m.height-i.height<=k.bottom?d[1]="top":k.bottom+o<0&&m.height-i.height<=k.top&&(d[1]="bottom")}}switch(d[0]){case"top":j.top=i.top-h;break;case"bottom":j.top=i.top+i.height;break;case"left":j.left=i.left-g;break;case"right":j.left=i.left+i.width}switch(d[1]){case"top":j.top=i.top;break;case"bottom":j.top=i.top+i.height-h;break;case"left":j.left=i.left;break;case"right":j.left=i.left+i.width-g;break;case"center":f.vertical.test(d[0])?j.left=i.left+i.width/2-g/2:j.top=i.top+i.height/2-h/2}return j.top=Math.round(j.top),j.left=Math.round(j.left),j.placement="center"===d[1]?d[0]:d[0]+"-"+d[1],j},positionArrow:function(a,c){a=this.getRawNode(a);var d=a.querySelector(".tooltip-inner, .popover-inner");if(d){var e=angular.element(d).hasClass("tooltip-inner"),g=e?a.querySelector(".tooltip-arrow"):a.querySelector(".arrow");if(g){var h={top:"",bottom:"",left:"",right:""};if(c=this.parsePlacement(c),"center"===c[1])return void angular.element(g).css(h);var i="border-"+c[0]+"-width",j=b.getComputedStyle(g)[i],k="border-";k+=f.vertical.test(c[0])?c[0]+"-"+c[1]:c[1]+"-"+c[0],k+="-radius";var l=b.getComputedStyle(e?d:a)[k];switch(c[0]){case"top":h.bottom=e?"0":"-"+j;break;case"bottom":h.top=e?"0":"-"+j;break;case"left":h.right=e?"0":"-"+j;break;case"right":h.left=e?"0":"-"+j}h[c[1]]=l,angular.element(g).css(h)}}}}}]),angular.module("ui.bootstrap.datepickerPopup",["ui.bootstrap.datepicker","ui.bootstrap.position"]).value("$datepickerPopupLiteralWarning",!0).constant("uibDatepickerPopupConfig",{altInputFormats:[],appendToBody:!1,clearText:"Clear",closeOnDateSelection:!0,closeText:"Done",currentText:"Today",datepickerPopup:"yyyy-MM-dd",datepickerPopupTemplateUrl:"uib/template/datepickerPopup/popup.html",datepickerTemplateUrl:"uib/template/datepicker/datepicker.html",html5Types:{date:"yyyy-MM-dd","datetime-local":"yyyy-MM-ddTHH:mm:ss.sss",month:"yyyy-MM"},onOpenFocus:!0,showButtonBar:!0,placement:"auto bottom-left"}).controller("UibDatepickerPopupController",["$scope","$element","$attrs","$compile","$log","$parse","$window","$document","$rootScope","$uibPosition","dateFilter","uibDateParser","uibDatepickerPopupConfig","$timeout","uibDatepickerConfig","$datepickerPopupLiteralWarning",function(a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p){function q(b){var c=l.parse(b,w,a.date);if(isNaN(c))for(var d=0;d
        "),G?(J=G.timezone,a.ngModelOptions=angular.copy(G),a.ngModelOptions.timezone=null,a.ngModelOptions.updateOnDefault===!0&&(a.ngModelOptions.updateOn=a.ngModelOptions.updateOn?a.ngModelOptions.updateOn+" default":"default"),C.attr("ng-model-options","ngModelOptions")):J=null,C.attr({"ng-model":"date","ng-change":"dateSelection(date)","template-url":A}),D=angular.element(C.children()[0]),D.attr("template-url",B),a.datepickerOptions||(a.datepickerOptions={}),K&&"month"===c.type&&(a.datepickerOptions.datepickerMode="month",a.datepickerOptions.minMode="month"),D.attr("datepicker-options","datepickerOptions"),K?F.$formatters.push(function(b){return a.date=l.fromTimezone(b,J),b}):(F.$$parserName="date",F.$validators.date=s,F.$parsers.unshift(r),F.$formatters.push(function(b){return F.$isEmpty(b)?(a.date=b,b):(a.date=l.fromTimezone(b,J),angular.isNumber(a.date)&&(a.date=new Date(a.date)),l.filter(a.date,w))})),F.$viewChangeListeners.push(function(){a.date=q(F.$viewValue)}),b.on("keydown",u),H=d(C)(a),C.remove(),y?h.find("body").append(H):b.after(H),a.$on("$destroy",function(){for(a.isOpen===!0&&(i.$$phase||a.$apply(function(){a.isOpen=!1})),H.remove(),b.off("keydown",u),h.off("click",t),E&&E.off("scroll",v),angular.element(g).off("resize",v);L.length;)L.shift()()})},a.getText=function(b){return a[b+"Text"]||m[b+"Text"]},a.isDisabled=function(b){"today"===b&&(b=l.fromTimezone(new Date,J));var c={};return angular.forEach(["minDate","maxDate"],function(b){a.datepickerOptions[b]?angular.isDate(a.datepickerOptions[b])?c[b]=l.fromTimezone(new Date(a.datepickerOptions[b]),J):(p&&e.warn("Literal date support has been deprecated, please switch to date object usage"),c[b]=new Date(k(a.datepickerOptions[b],"medium"))):c[b]=null}),a.datepickerOptions&&c.minDate&&a.compare(b,c.minDate)<0||c.maxDate&&a.compare(b,c.maxDate)>0},a.compare=function(a,b){return new Date(a.getFullYear(),a.getMonth(),a.getDate())-new Date(b.getFullYear(),b.getMonth(),b.getDate())},a.dateSelection=function(c){angular.isDefined(c)&&(a.date=c);var d=a.date?l.filter(a.date,w):null;b.val(d),F.$setViewValue(d),x&&(a.isOpen=!1,b[0].focus())},a.keydown=function(c){27===c.which&&(c.stopPropagation(),a.isOpen=!1,b[0].focus())},a.select=function(b,c){if(c.stopPropagation(),"today"===b){var d=new Date;angular.isDate(a.date)?(b=new Date(a.date),b.setFullYear(d.getFullYear(),d.getMonth(),d.getDate())):b=new Date(d.setHours(0,0,0,0))}a.dateSelection(b)},a.close=function(c){c.stopPropagation(),a.isOpen=!1,b[0].focus()},a.disabled=angular.isDefined(c.disabled)||!1,c.ngDisabled&&L.push(a.$parent.$watch(f(c.ngDisabled),function(b){a.disabled=b})),a.$watch("isOpen",function(d){d?a.disabled?a.isOpen=!1:n(function(){v(),z&&a.$broadcast("uib:datepicker.focus"),h.on("click",t);var d=c.popupPlacement?c.popupPlacement:m.placement;y||j.parsePlacement(d)[2]?(E=E||angular.element(j.scrollParent(b)),E&&E.on("scroll",v)):E=null,angular.element(g).on("resize",v)},0,!1):(h.off("click",t),E&&E.off("scroll",v),angular.element(g).off("resize",v))}),a.$on("uib:datepicker.mode",function(){n(v,0,!1)})}]).directive("uibDatepickerPopup",function(){return{require:["ngModel","uibDatepickerPopup"],controller:"UibDatepickerPopupController",scope:{datepickerOptions:"=?",isOpen:"=?",currentText:"@",clearText:"@",closeText:"@"},link:function(a,b,c,d){var e=d[0],f=d[1];f.init(e)}}}).directive("uibDatepickerPopupWrap",function(){return{replace:!0,transclude:!0,templateUrl:function(a,b){return b.templateUrl||"uib/template/datepickerPopup/popup.html"}}}),angular.module("ui.bootstrap.debounce",[]).factory("$$debounce",["$timeout",function(a){return function(b,c){var d;return function(){var e=this,f=Array.prototype.slice.call(arguments);d&&a.cancel(d),d=a(function(){b.apply(e,f)},c)}}}]),angular.module("ui.bootstrap.dropdown",["ui.bootstrap.position"]).constant("uibDropdownConfig",{appendToOpenClass:"uib-dropdown-open",openClass:"open"}).service("uibDropdownService",["$document","$rootScope",function(a,b){var c=null;this.open=function(b,f){c||(a.on("click",d),f.on("keydown",e)),c&&c!==b&&(c.isOpen=!1),c=b},this.close=function(b,f){c===b&&(c=null,a.off("click",d),f.off("keydown",e))};var d=function(a){if(c&&!(a&&"disabled"===c.getAutoClose()||a&&3===a.which)){var d=c.getToggleElement();if(!(a&&d&&d[0].contains(a.target))){var e=c.getDropdownElement();a&&"outsideClick"===c.getAutoClose()&&e&&e[0].contains(a.target)||(c.isOpen=!1,b.$$phase||c.$apply())}}},e=function(a){27===a.which?(a.stopPropagation(),c.focusToggleElement(),d()):c.isKeynavEnabled()&&-1!==[38,40].indexOf(a.which)&&c.isOpen&&(a.preventDefault(),a.stopPropagation(),c.focusDropdownEntry(a.which))}}]).controller("UibDropdownController",["$scope","$element","$attrs","$parse","uibDropdownConfig","uibDropdownService","$animate","$uibPosition","$document","$compile","$templateRequest",function(a,b,c,d,e,f,g,h,i,j,k){var l,m,n=this,o=a.$new(),p=e.appendToOpenClass,q=e.openClass,r=angular.noop,s=c.onToggle?d(c.onToggle):angular.noop,t=!1,u=null,v=!1,w=i.find("body");b.addClass("dropdown"),this.init=function(){if(c.isOpen&&(m=d(c.isOpen),r=m.assign,a.$watch(m,function(a){o.isOpen=!!a})),angular.isDefined(c.dropdownAppendTo)){var e=d(c.dropdownAppendTo)(o);e&&(u=angular.element(e))}t=angular.isDefined(c.dropdownAppendToBody),v=angular.isDefined(c.keyboardNav),t&&!u&&(u=w),u&&n.dropdownMenu&&(u.append(n.dropdownMenu),b.on("$destroy",function(){n.dropdownMenu.remove()}))},this.toggle=function(a){return o.isOpen=arguments.length?!!a:!o.isOpen,angular.isFunction(r)&&r(o,o.isOpen),o.isOpen},this.isOpen=function(){return o.isOpen},o.getToggleElement=function(){return n.toggleElement},o.getAutoClose=function(){return c.autoClose||"always"},o.getElement=function(){return b},o.isKeynavEnabled=function(){return v},o.focusDropdownEntry=function(a){var c=n.dropdownMenu?angular.element(n.dropdownMenu).find("a"):b.find("ul").eq(0).find("a");switch(a){case 40:angular.isNumber(n.selectedOption)?n.selectedOption=n.selectedOption===c.length-1?n.selectedOption:n.selectedOption+1:n.selectedOption=0;break;case 38:angular.isNumber(n.selectedOption)?n.selectedOption=0===n.selectedOption?0:n.selectedOption-1:n.selectedOption=c.length-1}c[n.selectedOption].focus()},o.getDropdownElement=function(){return n.dropdownMenu},o.focusToggleElement=function(){n.toggleElement&&n.toggleElement[0].focus()},o.$watch("isOpen",function(c,d){if(u&&n.dropdownMenu){var e,i,m=h.positionElements(b,n.dropdownMenu,"bottom-left",!0);if(e={top:m.top+"px",display:c?"block":"none"},i=n.dropdownMenu.hasClass("dropdown-menu-right"),i?(e.left="auto",e.right=window.innerWidth-(m.left+b.prop("offsetWidth"))+"px"):(e.left=m.left+"px",e.right="auto"),!t){var v=h.offset(u);e.top=m.top-v.top+"px",i?e.right=window.innerWidth-(m.left-v.left+b.prop("offsetWidth"))+"px":e.left=m.left-v.left+"px"}n.dropdownMenu.css(e)}var w=u?u:b,x=w.hasClass(u?p:q);if(x===!c&&g[c?"addClass":"removeClass"](w,u?p:q).then(function(){angular.isDefined(c)&&c!==d&&s(a,{open:!!c})}),c)n.dropdownMenuTemplateUrl&&k(n.dropdownMenuTemplateUrl).then(function(a){l=o.$new(),j(a.trim())(l,function(a){var b=a;n.dropdownMenu.replaceWith(b),n.dropdownMenu=b})}),o.focusToggleElement(),f.open(o,b);else{if(n.dropdownMenuTemplateUrl){l&&l.$destroy();var y=angular.element('');n.dropdownMenu.replaceWith(y),n.dropdownMenu=y}f.close(o,b),n.selectedOption=null}angular.isFunction(r)&&r(a,c)})}]).directive("uibDropdown",function(){return{controller:"UibDropdownController",link:function(a,b,c,d){d.init()}}}).directive("uibDropdownMenu",function(){return{restrict:"A",require:"?^uibDropdown",link:function(a,b,c,d){if(d&&!angular.isDefined(c.dropdownNested)){b.addClass("dropdown-menu");var e=c.templateUrl;e&&(d.dropdownMenuTemplateUrl=e),d.dropdownMenu||(d.dropdownMenu=b)}}}}).directive("uibDropdownToggle",function(){return{require:"?^uibDropdown",link:function(a,b,c,d){if(d){b.addClass("dropdown-toggle"),d.toggleElement=b;var e=function(e){e.preventDefault(),b.hasClass("disabled")||c.disabled||a.$apply(function(){d.toggle()})};b.bind("click",e),b.attr({"aria-haspopup":!0,"aria-expanded":!1}),a.$watch(d.isOpen,function(a){b.attr("aria-expanded",!!a)}),a.$on("$destroy",function(){b.unbind("click",e)})}}}}),angular.module("ui.bootstrap.stackedMap",[]).factory("$$stackedMap",function(){return{createNew:function(){var a=[];return{add:function(b,c){a.push({key:b,value:c})},get:function(b){for(var c=0;c-1&&y>a&&(a=y),a}function l(a,b){var c=v.get(a).value,d=c.appendTo;v.remove(a),z=v.top(),z&&(y=parseInt(z.value.modalDomEl.attr("index"),10)),o(c.modalDomEl,c.modalScope,function(){var b=c.openedClass||u;w.remove(b,a);var e=w.hasKey(b);d.toggleClass(b,e),!e&&t&&t.heightOverflow&&t.scrollbarWidth&&(t.originalRight?d.css({paddingRight:t.originalRight+"px"}):d.css({paddingRight:""}),t=null),m(!0)},c.closedDeferred),n(),b&&b.focus?b.focus():d.focus&&d.focus()}function m(a){var b;v.length()>0&&(b=v.top().value,b.modalDomEl.toggleClass(b.windowTopClass||"",a))}function n(){if(r&&-1===k()){var a=s;o(r,s,function(){a=null}),r=void 0,s=void 0}}function o(b,c,d,e){function g(){g.done||(g.done=!0,a.leave(b).then(function(){b.remove(),e&&e.resolve()}),c.$destroy(),d&&d())}var h,i=null,j=function(){return h||(h=f.defer(),i=h.promise),function(){h.resolve()}};return c.$broadcast(x.NOW_CLOSING_EVENT,j),f.when(i).then(g)}function p(a){if(a.isDefaultPrevented())return a;var b=v.top();if(b)switch(a.which){case 27:b.value.keyboard&&(a.preventDefault(),e.$apply(function(){x.dismiss(b.key,"escape key press")}));break;case 9:var c=x.loadFocusElementList(b),d=!1;a.shiftKey?(x.isFocusInFirstItem(a,c)||x.isModalFocused(a,b))&&(d=x.focusLastFocusableElement(c)):x.isFocusInLastItem(a,c)&&(d=x.focusFirstFocusableElement(c)),d&&(a.preventDefault(),a.stopPropagation())}}function q(a,b,c){return!a.value.modalScope.$broadcast("modal.closing",b,c).defaultPrevented}var r,s,t,u="modal-open",v=h.createNew(),w=g.createNew(),x={NOW_CLOSING_EVENT:"modal.stack.now-closing"},y=0,z=null,A="a[href], area[href], input:not([disabled]), button:not([disabled]),select:not([disabled]), textarea:not([disabled]), iframe, object, embed, *[tabindex], *[contenteditable=true]";return e.$watch(k,function(a){s&&(s.index=a)}),c.on("keydown",p),e.$on("$destroy",function(){c.off("keydown",p)}),x.open=function(b,f){var g=c[0].activeElement,h=f.openedClass||u;m(!1),z=v.top(),v.add(b,{deferred:f.deferred,renderDeferred:f.renderDeferred,closedDeferred:f.closedDeferred,modalScope:f.scope,backdrop:f.backdrop,keyboard:f.keyboard,openedClass:f.openedClass,windowTopClass:f.windowTopClass,animation:f.animation,appendTo:f.appendTo}),w.put(h,b);var j=f.appendTo,l=k();if(!j.length)throw new Error("appendTo element not found. Make sure that the element passed is in DOM.");l>=0&&!r&&(s=e.$new(!0),s.modalOptions=f,s.index=l,r=angular.element('
        '),r.attr("backdrop-class",f.backdropClass),f.animation&&r.attr("modal-animation","true"),d(r)(s),a.enter(r,j),t=i.scrollbarPadding(j),t.heightOverflow&&t.scrollbarWidth&&j.css({paddingRight:t.right+"px"})),y=z?parseInt(z.value.modalDomEl.attr("index"),10)+1:0;var n=angular.element('
        ');n.attr({"template-url":f.windowTemplateUrl,"window-class":f.windowClass,"window-top-class":f.windowTopClass,size:f.size,index:y,animate:"animate"}).html(f.content),f.animation&&n.attr("modal-animation","true"),j.addClass(h),a.enter(d(n)(f.scope),j),v.top().value.modalDomEl=n,v.top().value.modalOpener=g},x.close=function(a,b){var c=v.get(a);return c&&q(c,b,!0)?(c.value.modalScope.$$uibDestructionScheduled=!0,c.value.deferred.resolve(b),l(a,c.value.modalOpener),!0):!c},x.dismiss=function(a,b){var c=v.get(a);return c&&q(c,b,!1)?(c.value.modalScope.$$uibDestructionScheduled=!0,c.value.deferred.reject(b),l(a,c.value.modalOpener),!0):!c},x.dismissAll=function(a){for(var b=this.getTop();b&&this.dismiss(b.key,a);)b=this.getTop()},x.getTop=function(){return v.top()},x.modalRendered=function(a){var b=v.get(a);b&&b.value.renderDeferred.resolve()},x.focusFirstFocusableElement=function(a){return a.length>0?(a[0].focus(),!0):!1},x.focusLastFocusableElement=function(a){return a.length>0?(a[a.length-1].focus(),!0):!1},x.isModalFocused=function(a,b){if(a&&b){var c=b.value.modalDomEl;if(c&&c.length)return(a.target||a.srcElement)===c[0]}return!1},x.isFocusInFirstItem=function(a,b){return b.length>0?(a.target||a.srcElement)===b[0]:!1},x.isFocusInLastItem=function(a,b){return b.length>0?(a.target||a.srcElement)===b[b.length-1]:!1},x.loadFocusElementList=function(a){if(a){var b=a.value.modalDomEl;if(b&&b.length){var c=b[0].querySelectorAll(A);return c?Array.prototype.filter.call(c,function(a){return j(a)}):c}}},x}]).provider("$uibModal",function(){var a={options:{animation:!0,backdrop:!0,keyboard:!0},$get:["$rootScope","$q","$document","$templateRequest","$controller","$uibResolve","$uibModalStack",function(b,c,d,e,f,g,h){function i(a){return a.template?c.when(a.template):e(angular.isFunction(a.templateUrl)?a.templateUrl():a.templateUrl)}var j={},k=null;return j.getPromiseChain=function(){return k},j.open=function(e){function j(){return r}var l=c.defer(),m=c.defer(),n=c.defer(),o=c.defer(),p={result:l.promise,opened:m.promise,closed:n.promise,rendered:o.promise,close:function(a){return h.close(p,a)},dismiss:function(a){return h.dismiss(p,a)}};if(e=angular.extend({},a.options,e),e.resolve=e.resolve||{},e.appendTo=e.appendTo||d.find("body").eq(0),!e.template&&!e.templateUrl)throw new Error("One of template or templateUrl options is required.");var q,r=c.all([i(e),g.resolve(e.resolve,{},null,null)]);return q=k=c.all([k]).then(j,j).then(function(a){var c=e.scope||b,d=c.$new();d.$close=p.close,d.$dismiss=p.dismiss,d.$on("$destroy",function(){d.$$uibDestructionScheduled||d.$dismiss("$uibUnscheduledDestruction")});var g,i,j={};e.controller&&(j.$scope=d,j.$uibModalInstance=p,angular.forEach(a[1],function(a,b){j[b]=a}),i=f(e.controller,j,!0),e.controllerAs?(g=i.instance,e.bindToController&&(g.$close=d.$close,g.$dismiss=d.$dismiss,angular.extend(g,c)),g=i(),d[e.controllerAs]=g):g=i(),angular.isFunction(g.$onInit)&&g.$onInit()),h.open(p,{scope:d,deferred:l,renderDeferred:o,closedDeferred:n,content:a[0],animation:e.animation,backdrop:e.backdrop,keyboard:e.keyboard,backdropClass:e.backdropClass,windowTopClass:e.windowTopClass,windowClass:e.windowClass,windowTemplateUrl:e.windowTemplateUrl,size:e.size,openedClass:e.openedClass,appendTo:e.appendTo}),m.resolve(!0)},function(a){m.reject(a),l.reject(a)})["finally"](function(){k===q&&(k=null)}),p},j}]};return a}),angular.module("ui.bootstrap.paging",[]).factory("uibPaging",["$parse",function(a){return{create:function(b,c,d){b.setNumPages=d.numPages?a(d.numPages).assign:angular.noop,b.ngModelCtrl={$setViewValue:angular.noop},b._watchers=[],b.init=function(a,e){b.ngModelCtrl=a,b.config=e,a.$render=function(){b.render()},d.itemsPerPage?b._watchers.push(c.$parent.$watch(d.itemsPerPage,function(a){b.itemsPerPage=parseInt(a,10),c.totalPages=b.calculateTotalPages(),b.updatePage()})):b.itemsPerPage=e.itemsPerPage,c.$watch("totalItems",function(a,d){(angular.isDefined(a)||a!==d)&&(c.totalPages=b.calculateTotalPages(),b.updatePage())})},b.calculateTotalPages=function(){var a=b.itemsPerPage<1?1:Math.ceil(c.totalItems/b.itemsPerPage);return Math.max(a||0,1)},b.render=function(){c.page=parseInt(b.ngModelCtrl.$viewValue,10)||1},c.selectPage=function(a,d){d&&d.preventDefault();var e=!c.ngDisabled||!d;e&&c.page!==a&&a>0&&a<=c.totalPages&&(d&&d.target&&d.target.blur(),b.ngModelCtrl.$setViewValue(a),b.ngModelCtrl.$render())},c.getText=function(a){return c[a+"Text"]||b.config[a+"Text"]},c.noPrevious=function(){return 1===c.page},c.noNext=function(){return c.page===c.totalPages},b.updatePage=function(){b.setNumPages(c.$parent,c.totalPages),c.page>c.totalPages?c.selectPage(c.totalPages):b.ngModelCtrl.$render()},c.$on("$destroy",function(){for(;b._watchers.length;)b._watchers.shift()()})}}}]),angular.module("ui.bootstrap.pager",["ui.bootstrap.paging"]).controller("UibPagerController",["$scope","$attrs","uibPaging","uibPagerConfig",function(a,b,c,d){a.align=angular.isDefined(b.align)?a.$parent.$eval(b.align):d.align,c.create(this,a,b)}]).constant("uibPagerConfig",{itemsPerPage:10,previousText:"« Previous",nextText:"Next »",align:!0}).directive("uibPager",["uibPagerConfig",function(a){return{scope:{totalItems:"=",previousText:"@",nextText:"@",ngDisabled:"="},require:["uibPager","?ngModel"],controller:"UibPagerController",controllerAs:"pager",templateUrl:function(a,b){return b.templateUrl||"uib/template/pager/pager.html"},replace:!0,link:function(b,c,d,e){var f=e[0],g=e[1];g&&f.init(g,a)}}}]),angular.module("ui.bootstrap.pagination",["ui.bootstrap.paging"]).controller("UibPaginationController",["$scope","$attrs","$parse","uibPaging","uibPaginationConfig",function(a,b,c,d,e){function f(a,b,c){return{number:a,text:b,active:c}}function g(a,b){var c=[],d=1,e=b,g=angular.isDefined(i)&&b>i;g&&(j?(d=Math.max(a-Math.floor(i/2),1),e=d+i-1,e>b&&(e=b,d=e-i+1)):(d=(Math.ceil(a/i)-1)*i+1,e=Math.min(d+i-1,b)));for(var h=d;e>=h;h++){var n=f(h,m(h),h===a);c.push(n)}if(g&&i>0&&(!j||k||l)){if(d>1){if(!l||d>3){var o=f(d-1,"...",!1);c.unshift(o)}if(l){if(3===d){var p=f(2,"2",!1);c.unshift(p)}var q=f(1,"1",!1);c.unshift(q)}}if(b>e){if(!l||b-2>e){var r=f(e+1,"...",!1);c.push(r)}if(l){if(e===b-2){var s=f(b-1,b-1,!1);c.push(s)}var t=f(b,b,!1);c.push(t)}}}return c}var h=this,i=angular.isDefined(b.maxSize)?a.$parent.$eval(b.maxSize):e.maxSize,j=angular.isDefined(b.rotate)?a.$parent.$eval(b.rotate):e.rotate,k=angular.isDefined(b.forceEllipses)?a.$parent.$eval(b.forceEllipses):e.forceEllipses,l=angular.isDefined(b.boundaryLinkNumbers)?a.$parent.$eval(b.boundaryLinkNumbers):e.boundaryLinkNumbers,m=angular.isDefined(b.pageLabel)?function(c){return a.$parent.$eval(b.pageLabel,{$page:c})}:angular.identity;a.boundaryLinks=angular.isDefined(b.boundaryLinks)?a.$parent.$eval(b.boundaryLinks):e.boundaryLinks,a.directionLinks=angular.isDefined(b.directionLinks)?a.$parent.$eval(b.directionLinks):e.directionLinks,d.create(this,a,b),b.maxSize&&h._watchers.push(a.$parent.$watch(c(b.maxSize),function(a){i=parseInt(a,10),h.render()}));var n=this.render;this.render=function(){n(),a.page>0&&a.page<=a.totalPages&&(a.pages=g(a.page,a.totalPages))}}]).constant("uibPaginationConfig",{itemsPerPage:10,boundaryLinks:!1,boundaryLinkNumbers:!1,directionLinks:!0,firstText:"First",previousText:"Previous",nextText:"Next",lastText:"Last",rotate:!0,forceEllipses:!1}).directive("uibPagination",["$parse","uibPaginationConfig",function(a,b){return{scope:{totalItems:"=",firstText:"@",previousText:"@",nextText:"@",lastText:"@",ngDisabled:"="},require:["uibPagination","?ngModel"],controller:"UibPaginationController",controllerAs:"pagination",templateUrl:function(a,b){return b.templateUrl||"uib/template/pagination/pagination.html"},replace:!0,link:function(a,c,d,e){var f=e[0],g=e[1];g&&f.init(g,b)}}}]),angular.module("ui.bootstrap.tooltip",["ui.bootstrap.position","ui.bootstrap.stackedMap"]).provider("$uibTooltip",function(){function a(a){var b=/[A-Z]/g,c="-";return a.replace(b,function(a,b){return(b?c:"")+a.toLowerCase()})}var b={placement:"top",placementClassPrefix:"",animation:!0,popupDelay:0,popupCloseDelay:0,useContentExp:!1},c={mouseenter:"mouseleave",click:"click",outsideClick:"outsideClick",focus:"blur",none:""},d={};this.options=function(a){angular.extend(d,a)},this.setTriggers=function(a){angular.extend(c,a)},this.$get=["$window","$compile","$timeout","$document","$uibPosition","$interpolate","$rootScope","$parse","$$stackedMap",function(e,f,g,h,i,j,k,l,m){function n(a){if(27===a.which){var b=o.top();b&&(b.value.close(),o.removeTop(),b=null)}}var o=m.createNew();return h.on("keypress",n),k.$on("$destroy",function(){h.off("keypress",n)}),function(e,k,m,n){function p(a){var b=(a||n.trigger||m).split(" "),d=b.map(function(a){return c[a]||a});return{show:b,hide:d}}n=angular.extend({},b,d,n);var q=a(e),r=j.startSymbol(),s=j.endSymbol(),t="
        ';return{compile:function(a,b){var c=f(t);return function(a,b,d,f){function j(){N.isOpen?q():m()}function m(){M&&!a.$eval(d[k+"Enable"])||(u(),x(),N.popupDelay?G||(G=g(r,N.popupDelay,!1)):r())}function q(){s(),N.popupCloseDelay?H||(H=g(t,N.popupCloseDelay,!1)):t()}function r(){return s(),u(),N.content?(v(),void N.$evalAsync(function(){N.isOpen=!0,y(!0),S()})):angular.noop}function s(){G&&(g.cancel(G),G=null),I&&(g.cancel(I),I=null)}function t(){N&&N.$evalAsync(function(){N&&(N.isOpen=!1,y(!1),N.animation?F||(F=g(w,150,!1)):w())})}function u(){H&&(g.cancel(H),H=null),F&&(g.cancel(F),F=null)}function v(){D||(E=N.$new(),D=c(E,function(a){K?h.find("body").append(a):b.after(a)}),z())}function w(){s(),u(),A(),D&&(D.remove(),D=null),E&&(E.$destroy(),E=null)}function x(){N.title=d[k+"Title"],Q?N.content=Q(a):N.content=d[e],N.popupClass=d[k+"Class"],N.placement=angular.isDefined(d[k+"Placement"])?d[k+"Placement"]:n.placement;var b=i.parsePlacement(N.placement);J=b[1]?b[0]+"-"+b[1]:b[0];var c=parseInt(d[k+"PopupDelay"],10),f=parseInt(d[k+"PopupCloseDelay"],10); +N.popupDelay=isNaN(c)?n.popupDelay:c,N.popupCloseDelay=isNaN(f)?n.popupCloseDelay:f}function y(b){P&&angular.isFunction(P.assign)&&P.assign(a,b)}function z(){R.length=0,Q?(R.push(a.$watch(Q,function(a){N.content=a,!a&&N.isOpen&&t()})),R.push(E.$watch(function(){O||(O=!0,E.$$postDigest(function(){O=!1,N&&N.isOpen&&S()}))}))):R.push(d.$observe(e,function(a){N.content=a,!a&&N.isOpen?t():S()})),R.push(d.$observe(k+"Title",function(a){N.title=a,N.isOpen&&S()})),R.push(d.$observe(k+"Placement",function(a){N.placement=a?a:n.placement,N.isOpen&&S()}))}function A(){R.length&&(angular.forEach(R,function(a){a()}),R.length=0)}function B(a){N&&N.isOpen&&D&&(b[0].contains(a.target)||D[0].contains(a.target)||q())}function C(){var a=d[k+"Trigger"];T(),L=p(a),"none"!==L.show&&L.show.forEach(function(a,c){"outsideClick"===a?(b.on("click",j),h.on("click",B)):a===L.hide[c]?b.on(a,j):a&&(b.on(a,m),b.on(L.hide[c],q)),b.on("keypress",function(a){27===a.which&&q()})})}var D,E,F,G,H,I,J,K=angular.isDefined(n.appendToBody)?n.appendToBody:!1,L=p(void 0),M=angular.isDefined(d[k+"Enable"]),N=a.$new(!0),O=!1,P=angular.isDefined(d[k+"IsOpen"])?l(d[k+"IsOpen"]):!1,Q=n.useContentExp?l(d[e]):!1,R=[],S=function(){D&&D.html()&&(I||(I=g(function(){var a=i.positionElements(b,D,N.placement,K);D.css({top:a.top+"px",left:a.left+"px"}),D.hasClass(a.placement.split("-")[0])||(D.removeClass(J.split("-")[0]),D.addClass(a.placement.split("-")[0])),D.hasClass(n.placementClassPrefix+a.placement)||(D.removeClass(n.placementClassPrefix+J),D.addClass(n.placementClassPrefix+a.placement)),D.hasClass("uib-position-measure")?(i.positionArrow(D,a.placement),D.removeClass("uib-position-measure")):J!==a.placement&&i.positionArrow(D,a.placement),J=a.placement,I=null},0,!1)))};N.origScope=a,N.isOpen=!1,o.add(N,{close:t}),N.contentExp=function(){return N.content},d.$observe("disabled",function(a){a&&s(),a&&N.isOpen&&t()}),P&&a.$watch(P,function(a){N&&!a===N.isOpen&&j()});var T=function(){L.show.forEach(function(a){"outsideClick"===a?b.off("click",j):(b.off(a,m),b.off(a,j))}),L.hide.forEach(function(a){"outsideClick"===a?h.off("click",B):b.off(a,q)})};C();var U=a.$eval(d[k+"Animation"]);N.animation=angular.isDefined(U)?!!U:n.animation;var V,W=k+"AppendToBody";V=W in d&&void 0===d[W]?!0:a.$eval(d[W]),K=angular.isDefined(V)?V:K,a.$on("$destroy",function(){T(),w(),o.remove(N),N=null})}}}}}]}).directive("uibTooltipTemplateTransclude",["$animate","$sce","$compile","$templateRequest",function(a,b,c,d){return{link:function(e,f,g){var h,i,j,k=e.$eval(g.tooltipTemplateTranscludeScope),l=0,m=function(){i&&(i.remove(),i=null),h&&(h.$destroy(),h=null),j&&(a.leave(j).then(function(){i=null}),i=j,j=null)};e.$watch(b.parseAsResourceUrl(g.uibTooltipTemplateTransclude),function(b){var g=++l;b?(d(b,!0).then(function(d){if(g===l){var e=k.$new(),i=d,n=c(i)(e,function(b){m(),a.enter(b,f)});h=e,j=n,h.$emit("$includeContentLoaded",b)}},function(){g===l&&(m(),e.$emit("$includeContentError",b))}),e.$emit("$includeContentRequested",b)):m()}),e.$on("$destroy",m)}}}]).directive("uibTooltipClasses",["$uibPosition",function(a){return{restrict:"A",link:function(b,c,d){if(b.placement){var e=a.parsePlacement(b.placement);c.addClass(e[0])}b.popupClass&&c.addClass(b.popupClass),b.animation()&&c.addClass(d.tooltipAnimationClass)}}}]).directive("uibTooltipPopup",function(){return{replace:!0,scope:{content:"@",placement:"@",popupClass:"@",animation:"&",isOpen:"&"},templateUrl:"uib/template/tooltip/tooltip-popup.html"}}).directive("uibTooltip",["$uibTooltip",function(a){return a("uibTooltip","tooltip","mouseenter")}]).directive("uibTooltipTemplatePopup",function(){return{replace:!0,scope:{contentExp:"&",placement:"@",popupClass:"@",animation:"&",isOpen:"&",originScope:"&"},templateUrl:"uib/template/tooltip/tooltip-template-popup.html"}}).directive("uibTooltipTemplate",["$uibTooltip",function(a){return a("uibTooltipTemplate","tooltip","mouseenter",{useContentExp:!0})}]).directive("uibTooltipHtmlPopup",function(){return{replace:!0,scope:{contentExp:"&",placement:"@",popupClass:"@",animation:"&",isOpen:"&"},templateUrl:"uib/template/tooltip/tooltip-html-popup.html"}}).directive("uibTooltipHtml",["$uibTooltip",function(a){return a("uibTooltipHtml","tooltip","mouseenter",{useContentExp:!0})}]),angular.module("ui.bootstrap.popover",["ui.bootstrap.tooltip"]).directive("uibPopoverTemplatePopup",function(){return{replace:!0,scope:{uibTitle:"@",contentExp:"&",placement:"@",popupClass:"@",animation:"&",isOpen:"&",originScope:"&"},templateUrl:"uib/template/popover/popover-template.html"}}).directive("uibPopoverTemplate",["$uibTooltip",function(a){return a("uibPopoverTemplate","popover","click",{useContentExp:!0})}]).directive("uibPopoverHtmlPopup",function(){return{replace:!0,scope:{contentExp:"&",uibTitle:"@",placement:"@",popupClass:"@",animation:"&",isOpen:"&"},templateUrl:"uib/template/popover/popover-html.html"}}).directive("uibPopoverHtml",["$uibTooltip",function(a){return a("uibPopoverHtml","popover","click",{useContentExp:!0})}]).directive("uibPopoverPopup",function(){return{replace:!0,scope:{uibTitle:"@",content:"@",placement:"@",popupClass:"@",animation:"&",isOpen:"&"},templateUrl:"uib/template/popover/popover.html"}}).directive("uibPopover",["$uibTooltip",function(a){return a("uibPopover","popover","click")}]),angular.module("ui.bootstrap.progressbar",[]).constant("uibProgressConfig",{animate:!0,max:100}).controller("UibProgressController",["$scope","$attrs","uibProgressConfig",function(a,b,c){function d(){return angular.isDefined(a.maxParam)?a.maxParam:c.max}var e=this,f=angular.isDefined(b.animate)?a.$parent.$eval(b.animate):c.animate;this.bars=[],a.max=d(),this.addBar=function(a,b,c){f||b.css({transition:"none"}),this.bars.push(a),a.max=d(),a.title=c&&angular.isDefined(c.title)?c.title:"progressbar",a.$watch("value",function(b){a.recalculatePercentage()}),a.recalculatePercentage=function(){var b=e.bars.reduce(function(a,b){return b.percent=+(100*b.value/b.max).toFixed(2),a+b.percent},0);b>100&&(a.percent-=b-100)},a.$on("$destroy",function(){b=null,e.removeBar(a)})},this.removeBar=function(a){this.bars.splice(this.bars.indexOf(a),1),this.bars.forEach(function(a){a.recalculatePercentage()})},a.$watch("maxParam",function(a){e.bars.forEach(function(a){a.max=d(),a.recalculatePercentage()})})}]).directive("uibProgress",function(){return{replace:!0,transclude:!0,controller:"UibProgressController",require:"uibProgress",scope:{maxParam:"=?max"},templateUrl:"uib/template/progressbar/progress.html"}}).directive("uibBar",function(){return{replace:!0,transclude:!0,require:"^uibProgress",scope:{value:"=",type:"@"},templateUrl:"uib/template/progressbar/bar.html",link:function(a,b,c,d){d.addBar(a,b,c)}}}).directive("uibProgressbar",function(){return{replace:!0,transclude:!0,controller:"UibProgressController",scope:{value:"=",maxParam:"=?max",type:"@"},templateUrl:"uib/template/progressbar/progressbar.html",link:function(a,b,c,d){d.addBar(a,angular.element(b.children()[0]),{title:c.title})}}}),angular.module("ui.bootstrap.rating",[]).constant("uibRatingConfig",{max:5,stateOn:null,stateOff:null,enableReset:!0,titles:["one","two","three","four","five"]}).controller("UibRatingController",["$scope","$attrs","uibRatingConfig",function(a,b,c){var d={$setViewValue:angular.noop},e=this;this.init=function(e){d=e,d.$render=this.render,d.$formatters.push(function(a){return angular.isNumber(a)&&a<<0!==a&&(a=Math.round(a)),a}),this.stateOn=angular.isDefined(b.stateOn)?a.$parent.$eval(b.stateOn):c.stateOn,this.stateOff=angular.isDefined(b.stateOff)?a.$parent.$eval(b.stateOff):c.stateOff,this.enableReset=angular.isDefined(b.enableReset)?a.$parent.$eval(b.enableReset):c.enableReset;var f=angular.isDefined(b.titles)?a.$parent.$eval(b.titles):c.titles;this.titles=angular.isArray(f)&&f.length>0?f:c.titles;var g=angular.isDefined(b.ratingStates)?a.$parent.$eval(b.ratingStates):new Array(angular.isDefined(b.max)?a.$parent.$eval(b.max):c.max);a.range=this.buildTemplateObjects(g)},this.buildTemplateObjects=function(a){for(var b=0,c=a.length;c>b;b++)a[b]=angular.extend({index:b},{stateOn:this.stateOn,stateOff:this.stateOff,title:this.getTitle(b)},a[b]);return a},this.getTitle=function(a){return a>=this.titles.length?a+1:this.titles[a]},a.rate=function(b){if(!a.readonly&&b>=0&&b<=a.range.length){var c=e.enableReset&&d.$viewValue===b?0:b;d.$setViewValue(c),d.$render()}},a.enter=function(b){a.readonly||(a.value=b),a.onHover({value:b})},a.reset=function(){a.value=d.$viewValue,a.onLeave()},a.onKeydown=function(b){/(37|38|39|40)/.test(b.which)&&(b.preventDefault(),b.stopPropagation(),a.rate(a.value+(38===b.which||39===b.which?1:-1)))},this.render=function(){a.value=d.$viewValue,a.title=e.getTitle(a.value-1)}}]).directive("uibRating",function(){return{require:["uibRating","ngModel"],scope:{readonly:"=?readOnly",onHover:"&",onLeave:"&"},controller:"UibRatingController",templateUrl:"uib/template/rating/rating.html",replace:!0,link:function(a,b,c,d){var e=d[0],f=d[1];e.init(f)}}}),angular.module("ui.bootstrap.tabs",[]).controller("UibTabsetController",["$scope",function(a){function b(a){for(var b=0;bb.index?1:a.index0&&13>b:b>=0&&24>b;return c&&""!==a.hours?(a.showMeridian&&(12===b&&(b=0),a.meridian===v[1]&&(b+=12)),b):void 0}function i(){var b=+a.minutes,c=b>=0&&60>b;return c&&""!==a.minutes?b:void 0}function j(){var b=+a.seconds;return b>=0&&60>b?b:void 0}function k(a,b){return null===a?"":angular.isDefined(a)&&a.toString().length<2&&!b?"0"+a:a.toString()}function l(a){m(),u.$setViewValue(new Date(s)),n(a)}function m(){u.$setValidity("time",!0),a.invalidHours=!1,a.invalidMinutes=!1,a.invalidSeconds=!1}function n(b){if(u.$modelValue){var c=s.getHours(),d=s.getMinutes(),e=s.getSeconds();a.showMeridian&&(c=0===c||12===c?12:c%12),a.hours="h"===b?c:k(c,!w),"m"!==b&&(a.minutes=k(d)),a.meridian=s.getHours()<12?v[0]:v[1],"s"!==b&&(a.seconds=k(e)),a.meridian=s.getHours()<12?v[0]:v[1]}else a.hours=null,a.minutes=null,a.seconds=null,a.meridian=v[0]}function o(a){s=q(s,a),l()}function p(a,b){return q(a,60*b)}function q(a,b){var c=new Date(a.getTime()+1e3*b),d=new Date(a);return d.setHours(c.getHours(),c.getMinutes(),c.getSeconds()),d}function r(){return(null===a.hours||""===a.hours)&&(null===a.minutes||""===a.minutes)&&(!a.showSeconds||a.showSeconds&&(null===a.seconds||""===a.seconds))}var s=new Date,t=[],u={$setViewValue:angular.noop},v=angular.isDefined(c.meridians)?a.$parent.$eval(c.meridians):g.meridians||f.DATETIME_FORMATS.AMPMS,w=angular.isDefined(c.padHours)?a.$parent.$eval(c.padHours):!0;a.tabindex=angular.isDefined(c.tabindex)?c.tabindex:0,b.removeAttr("tabindex"),this.init=function(b,d){u=b,u.$render=this.render,u.$formatters.unshift(function(a){return a?new Date(a):null});var e=d.eq(0),f=d.eq(1),h=d.eq(2),i=angular.isDefined(c.mousewheel)?a.$parent.$eval(c.mousewheel):g.mousewheel;i&&this.setupMousewheelEvents(e,f,h);var j=angular.isDefined(c.arrowkeys)?a.$parent.$eval(c.arrowkeys):g.arrowkeys;j&&this.setupArrowkeyEvents(e,f,h),a.readonlyInput=angular.isDefined(c.readonlyInput)?a.$parent.$eval(c.readonlyInput):g.readonlyInput,this.setupInputEvents(e,f,h)};var x=g.hourStep;c.hourStep&&t.push(a.$parent.$watch(d(c.hourStep),function(a){x=+a}));var y=g.minuteStep;c.minuteStep&&t.push(a.$parent.$watch(d(c.minuteStep),function(a){y=+a}));var z;t.push(a.$parent.$watch(d(c.min),function(a){var b=new Date(a);z=isNaN(b)?void 0:b}));var A;t.push(a.$parent.$watch(d(c.max),function(a){var b=new Date(a);A=isNaN(b)?void 0:b}));var B=!1;c.ngDisabled&&t.push(a.$parent.$watch(d(c.ngDisabled),function(a){B=a})),a.noIncrementHours=function(){var a=p(s,60*x);return B||a>A||s>a&&z>a},a.noDecrementHours=function(){var a=p(s,60*-x);return B||z>a||a>s&&a>A},a.noIncrementMinutes=function(){var a=p(s,y);return B||a>A||s>a&&z>a},a.noDecrementMinutes=function(){var a=p(s,-y);return B||z>a||a>s&&a>A},a.noIncrementSeconds=function(){var a=q(s,C);return B||a>A||s>a&&z>a},a.noDecrementSeconds=function(){var a=q(s,-C);return B||z>a||a>s&&a>A},a.noToggleMeridian=function(){return s.getHours()<12?B||p(s,720)>A:B||p(s,-720)0};b.bind("mousewheel wheel",function(b){B||a.$apply(e(b)?a.incrementHours():a.decrementHours()),b.preventDefault()}),c.bind("mousewheel wheel",function(b){B||a.$apply(e(b)?a.incrementMinutes():a.decrementMinutes()),b.preventDefault()}),d.bind("mousewheel wheel",function(b){B||a.$apply(e(b)?a.incrementSeconds():a.decrementSeconds()),b.preventDefault()})},this.setupArrowkeyEvents=function(b,c,d){b.bind("keydown",function(b){B||(38===b.which?(b.preventDefault(),a.incrementHours(),a.$apply()):40===b.which&&(b.preventDefault(),a.decrementHours(),a.$apply()))}),c.bind("keydown",function(b){B||(38===b.which?(b.preventDefault(),a.incrementMinutes(),a.$apply()):40===b.which&&(b.preventDefault(),a.decrementMinutes(),a.$apply()))}),d.bind("keydown",function(b){B||(38===b.which?(b.preventDefault(),a.incrementSeconds(),a.$apply()):40===b.which&&(b.preventDefault(),a.decrementSeconds(),a.$apply()))})},this.setupInputEvents=function(b,c,d){if(a.readonlyInput)return a.updateHours=angular.noop,a.updateMinutes=angular.noop,void(a.updateSeconds=angular.noop);var e=function(b,c,d){u.$setViewValue(null),u.$setValidity("time",!1),angular.isDefined(b)&&(a.invalidHours=b),angular.isDefined(c)&&(a.invalidMinutes=c),angular.isDefined(d)&&(a.invalidSeconds=d)};a.updateHours=function(){var a=h(),b=i();u.$setDirty(),angular.isDefined(a)&&angular.isDefined(b)?(s.setHours(a),s.setMinutes(b),z>s||s>A?e(!0):l("h")):e(!0)},b.bind("blur",function(b){u.$setTouched(),r()?m():null===a.hours||""===a.hours?e(!0):!a.invalidHours&&a.hours<10&&a.$apply(function(){a.hours=k(a.hours,!w)})}),a.updateMinutes=function(){var a=i(),b=h();u.$setDirty(),angular.isDefined(a)&&angular.isDefined(b)?(s.setHours(b),s.setMinutes(a),z>s||s>A?e(void 0,!0):l("m")):e(void 0,!0)},c.bind("blur",function(b){u.$setTouched(),r()?m():null===a.minutes?e(void 0,!0):!a.invalidMinutes&&a.minutes<10&&a.$apply(function(){a.minutes=k(a.minutes)})}),a.updateSeconds=function(){var a=j();u.$setDirty(),angular.isDefined(a)?(s.setSeconds(a),l("s")):e(void 0,void 0,!0)},d.bind("blur",function(b){r()?m():!a.invalidSeconds&&a.seconds<10&&a.$apply(function(){a.seconds=k(a.seconds)})})},this.render=function(){var b=u.$viewValue;isNaN(b)?(u.$setValidity("time",!1),e.error('Timepicker directive: "ng-model" value must be a Date object, a number of milliseconds since 01.01.1970 or a string representing an RFC2822 or ISO 8601 date.')):(b&&(s=b),z>s||s>A?(u.$setValidity("time",!1),a.invalidHours=!0,a.invalidMinutes=!0):m(),n())},a.showSpinners=angular.isDefined(c.showSpinners)?a.$parent.$eval(c.showSpinners):g.showSpinners,a.incrementHours=function(){a.noIncrementHours()||o(60*x*60)},a.decrementHours=function(){a.noDecrementHours()||o(60*-x*60)},a.incrementMinutes=function(){a.noIncrementMinutes()||o(60*y)},a.decrementMinutes=function(){a.noDecrementMinutes()||o(60*-y)},a.incrementSeconds=function(){a.noIncrementSeconds()||o(C)},a.decrementSeconds=function(){a.noDecrementSeconds()||o(-C)},a.toggleMeridian=function(){var b=i(),c=h();a.noToggleMeridian()||(angular.isDefined(b)&&angular.isDefined(c)?o(720*(s.getHours()<12?60:-60)):a.meridian=a.meridian===v[0]?v[1]:v[0])},a.blur=function(){u.$setTouched()},a.$on("$destroy",function(){for(;t.length;)t.shift()()})}]).directive("uibTimepicker",["uibTimepickerConfig",function(a){return{require:["uibTimepicker","?^ngModel"],controller:"UibTimepickerController",controllerAs:"timepicker",replace:!0,scope:{},templateUrl:function(b,c){return c.templateUrl||a.templateUrl},link:function(a,b,c,d){var e=d[0],f=d[1];f&&e.init(f,b.find("input"))}}}]),angular.module("ui.bootstrap.typeahead",["ui.bootstrap.debounce","ui.bootstrap.position"]).factory("uibTypeaheadParser",["$parse",function(a){var b=/^\s*([\s\S]+?)(?:\s+as\s+([\s\S]+?))?\s+for\s+(?:([\$\w][\$\w\d]*))\s+in\s+([\s\S]+?)$/;return{parse:function(c){var d=c.match(b);if(!d)throw new Error('Expected typeahead specification in form of "_modelValue_ (as _label_)? for _item_ in _collection_" but got "'+c+'".');return{itemName:d[3],source:a(d[4]),viewMapper:a(d[2]||d[1]),modelMapper:a(d[1])}}}}]).controller("UibTypeaheadController",["$scope","$element","$attrs","$compile","$parse","$q","$timeout","$document","$window","$rootScope","$$debounce","$uibPosition","uibTypeaheadParser",function(a,b,c,d,e,f,g,h,i,j,k,l,m){function n(){N.moveInProgress||(N.moveInProgress=!0,N.$digest()),Y()}function o(){N.position=D?l.offset(b):l.position(b),N.position.top+=b.prop("offsetHeight")}var p,q,r=[9,13,27,38,40],s=200,t=a.$eval(c.typeaheadMinLength);t||0===t||(t=1),a.$watch(c.typeaheadMinLength,function(a){t=a||0===a?a:1});var u=a.$eval(c.typeaheadWaitMs)||0,v=a.$eval(c.typeaheadEditable)!==!1;a.$watch(c.typeaheadEditable,function(a){v=a!==!1});var w,x,y=e(c.typeaheadLoading).assign||angular.noop,z=e(c.typeaheadOnSelect),A=angular.isDefined(c.typeaheadSelectOnBlur)?a.$eval(c.typeaheadSelectOnBlur):!1,B=e(c.typeaheadNoResults).assign||angular.noop,C=c.typeaheadInputFormatter?e(c.typeaheadInputFormatter):void 0,D=c.typeaheadAppendToBody?a.$eval(c.typeaheadAppendToBody):!1,E=c.typeaheadAppendTo?a.$eval(c.typeaheadAppendTo):null,F=a.$eval(c.typeaheadFocusFirst)!==!1,G=c.typeaheadSelectOnExact?a.$eval(c.typeaheadSelectOnExact):!1,H=e(c.typeaheadIsOpen).assign||angular.noop,I=a.$eval(c.typeaheadShowHint)||!1,J=e(c.ngModel),K=e(c.ngModel+"($$$p)"),L=function(b,c){return angular.isFunction(J(a))&&q&&q.$options&&q.$options.getterSetter?K(b,{$$$p:c}):J.assign(b,c)},M=m.parse(c.uibTypeahead),N=a.$new(),O=a.$on("$destroy",function(){N.$destroy()});N.$on("$destroy",O);var P="typeahead-"+N.$id+"-"+Math.floor(1e4*Math.random());b.attr({"aria-autocomplete":"list","aria-expanded":!1,"aria-owns":P});var Q,R;I&&(Q=angular.element("
        "),Q.css("position","relative"),b.after(Q),R=b.clone(),R.attr("placeholder",""),R.attr("tabindex","-1"),R.val(""),R.css({position:"absolute",top:"0px",left:"0px","border-color":"transparent","box-shadow":"none",opacity:1,background:"none 0% 0% / auto repeat scroll padding-box border-box rgb(255, 255, 255)",color:"#999"}),b.css({position:"relative","vertical-align":"top","background-color":"transparent"}),Q.append(R),R.after(b));var S=angular.element("
        ");S.attr({id:P,matches:"matches",active:"activeIdx",select:"select(activeIdx, evt)","move-in-progress":"moveInProgress",query:"query",position:"position","assign-is-open":"assignIsOpen(isOpen)",debounce:"debounceUpdate"}),angular.isDefined(c.typeaheadTemplateUrl)&&S.attr("template-url",c.typeaheadTemplateUrl),angular.isDefined(c.typeaheadPopupTemplateUrl)&&S.attr("popup-template-url",c.typeaheadPopupTemplateUrl);var T=function(){I&&R.val("")},U=function(){N.matches=[],N.activeIdx=-1,b.attr("aria-expanded",!1),T()},V=function(a){return P+"-option-"+a};N.$watch("activeIdx",function(a){0>a?b.removeAttr("aria-activedescendant"):b.attr("aria-activedescendant",V(a))});var W=function(a,b){return N.matches.length>b&&a?a.toUpperCase()===N.matches[b].label.toUpperCase():!1},X=function(c,d){var e={$viewValue:c};y(a,!0),B(a,!1),f.when(M.source(a,e)).then(function(f){var g=c===p.$viewValue;if(g&&w)if(f&&f.length>0){N.activeIdx=F?0:-1,B(a,!1),N.matches.length=0;for(var h=0;h0&&i.slice(0,c.length).toUpperCase()===c.toUpperCase()?R.val(c+i.slice(c.length)):R.val("")}}else U(),B(a,!0);g&&y(a,!1)},function(){U(),y(a,!1),B(a,!0)})};D&&(angular.element(i).on("resize",n),h.find("body").on("scroll",n));var Y=k(function(){N.matches.length&&o(),N.moveInProgress=!1},s);N.moveInProgress=!1,N.query=void 0;var Z,$=function(a){Z=g(function(){X(a)},u)},_=function(){Z&&g.cancel(Z)};U(),N.assignIsOpen=function(b){H(a,b)},N.select=function(d,e){var f,h,i={};x=!0,i[M.itemName]=h=N.matches[d].model,f=M.modelMapper(a,i),L(a,f),p.$setValidity("editable",!0),p.$setValidity("parse",!0),z(a,{$item:h,$model:f,$label:M.viewMapper(a,i),$event:e}),U(),N.$eval(c.typeaheadFocusOnSelect)!==!1&&g(function(){b[0].focus()},0,!1)},b.on("keydown",function(b){if(0!==N.matches.length&&-1!==r.indexOf(b.which)){if(-1===N.activeIdx&&(9===b.which||13===b.which)||9===b.which&&b.shiftKey)return U(),void N.$digest();b.preventDefault();var c;switch(b.which){case 9:case 13:N.$apply(function(){angular.isNumber(N.debounceUpdate)||angular.isObject(N.debounceUpdate)?k(function(){N.select(N.activeIdx,b)},angular.isNumber(N.debounceUpdate)?N.debounceUpdate:N.debounceUpdate["default"]):N.select(N.activeIdx,b)});break;case 27:b.stopPropagation(),U(),a.$digest();break;case 38:N.activeIdx=(N.activeIdx>0?N.activeIdx:N.matches.length)-1,N.$digest(),c=S.find("li")[N.activeIdx],c.parentNode.scrollTop=c.offsetTop;break;case 40:N.activeIdx=(N.activeIdx+1)%N.matches.length,N.$digest(),c=S.find("li")[N.activeIdx],c.parentNode.scrollTop=c.offsetTop}}}),b.bind("focus",function(a){w=!0,0!==t||p.$viewValue||g(function(){X(p.$viewValue,a)},0)}),b.bind("blur",function(a){A&&N.matches.length&&-1!==N.activeIdx&&!x&&(x=!0,N.$apply(function(){angular.isObject(N.debounceUpdate)&&angular.isNumber(N.debounceUpdate.blur)?k(function(){N.select(N.activeIdx,a)},N.debounceUpdate.blur):N.select(N.activeIdx,a)})),!v&&p.$error.editable&&(p.$setViewValue(),p.$setValidity("editable",!0),p.$setValidity("parse",!0),b.val("")),w=!1,x=!1});var aa=function(c){b[0]!==c.target&&3!==c.which&&0!==N.matches.length&&(U(),j.$$phase||a.$digest())};h.on("click",aa),a.$on("$destroy",function(){h.off("click",aa),(D||E)&&ba.remove(),D&&(angular.element(i).off("resize",n),h.find("body").off("scroll",n)),S.remove(),I&&Q.remove()});var ba=d(S)(N);D?h.find("body").append(ba):E?angular.element(E).eq(0).append(ba):b.after(ba),this.init=function(b,c){p=b,q=c,N.debounceUpdate=p.$options&&e(p.$options.debounce)(a),p.$parsers.unshift(function(b){return w=!0,0===t||b&&b.length>=t?u>0?(_(),$(b)):X(b):(y(a,!1),_(),U()),v?b:b?void p.$setValidity("editable",!1):(p.$setValidity("editable",!0),null)}),p.$formatters.push(function(b){var c,d,e={};return v||p.$setValidity("editable",!0),C?(e.$model=b,C(a,e)):(e[M.itemName]=b,c=M.viewMapper(a,e),e[M.itemName]=void 0,d=M.viewMapper(a,e),c!==d?c:b)})}}]).directive("uibTypeahead",function(){return{controller:"UibTypeaheadController",require:["ngModel","^?ngModelOptions","uibTypeahead"],link:function(a,b,c,d){d[2].init(d[0],d[1])}}}).directive("uibTypeaheadPopup",["$$debounce",function(a){return{scope:{matches:"=",query:"=",active:"=",position:"&",moveInProgress:"=",select:"&",assignIsOpen:"&",debounce:"&"},replace:!0,templateUrl:function(a,b){return b.popupTemplateUrl||"uib/template/typeahead/typeahead-popup.html"},link:function(b,c,d){b.templateUrl=d.templateUrl,b.isOpen=function(){var a=b.matches.length>0;return b.assignIsOpen({isOpen:a}),a},b.isActive=function(a){return b.active===a},b.selectActive=function(a){b.active=a},b.selectMatch=function(c,d){var e=b.debounce();angular.isNumber(e)||angular.isObject(e)?a(function(){b.select({activeIdx:c,evt:d})},angular.isNumber(e)?e:e["default"]):b.select({activeIdx:c,evt:d})}}}}]).directive("uibTypeaheadMatch",["$templateRequest","$compile","$parse",function(a,b,c){return{scope:{index:"=",match:"=",query:"="},link:function(d,e,f){var g=c(f.templateUrl)(d.$parent)||"uib/template/typeahead/typeahead-match.html";a(g).then(function(a){var c=angular.element(a.trim());e.replaceWith(c),b(c)(d)})}}}]).filter("uibTypeaheadHighlight",["$sce","$injector","$log",function(a,b,c){function d(a){return a.replace(/([.?*+^$[\]\\(){}|-])/g,"\\$1")}function e(a){return/<.*>/g.test(a)}var f;return f=b.has("$sanitize"),function(b,g){return!f&&e(b)&&c.warn("Unsafe use of typeahead please use ngSanitize"),b=g?(""+b).replace(new RegExp(d(g),"gi"),"$&"):b,f||(b=a.trustAsHtml(b)),b}}]),angular.module("ui.bootstrap.carousel").run(function(){!angular.$$csp().noInlineStyle&&!angular.$$uibCarouselCss&&angular.element(document).find("head").prepend(''),angular.$$uibCarouselCss=!0}),angular.module("ui.bootstrap.datepicker").run(function(){!angular.$$csp().noInlineStyle&&!angular.$$uibDatepickerCss&&angular.element(document).find("head").prepend(''),angular.$$uibDatepickerCss=!0}),angular.module("ui.bootstrap.position").run(function(){!angular.$$csp().noInlineStyle&&!angular.$$uibPositionCss&&angular.element(document).find("head").prepend(''),angular.$$uibPositionCss=!0}),angular.module("ui.bootstrap.datepickerPopup").run(function(){!angular.$$csp().noInlineStyle&&!angular.$$uibDatepickerpopupCss&&angular.element(document).find("head").prepend(''),angular.$$uibDatepickerpopupCss=!0}),angular.module("ui.bootstrap.tooltip").run(function(){!angular.$$csp().noInlineStyle&&!angular.$$uibTooltipCss&&angular.element(document).find("head").prepend(''), +angular.$$uibTooltipCss=!0}),angular.module("ui.bootstrap.timepicker").run(function(){!angular.$$csp().noInlineStyle&&!angular.$$uibTimepickerCss&&angular.element(document).find("head").prepend(''),angular.$$uibTimepickerCss=!0}),angular.module("ui.bootstrap.typeahead").run(function(){!angular.$$csp().noInlineStyle&&!angular.$$uibTypeaheadCss&&angular.element(document).find("head").prepend(''),angular.$$uibTypeaheadCss=!0}); \ No newline at end of file diff --git a/app/bower_components/angular-leaflet-directive/.bower.json b/app/bower_components/angular-leaflet-directive/.bower.json index 7773f70..7c3c36f 100644 --- a/app/bower_components/angular-leaflet-directive/.bower.json +++ b/app/bower_components/angular-leaflet-directive/.bower.json @@ -1,6 +1,9 @@ { "name": "angular-leaflet-directive", - "description": "AngularJS directive to embed an interact with maps managed by Leaflet library", + "author": "https://github.com/tombatossals/angular-leaflet-directive/graphs/contributors", + "description": "angular-leaflet-directive - An AngularJS directive to easily interact with Leaflet maps", + "version": "0.10.0", + "homepage": "http://tombatossals.github.io/angular-leaflet-directive/", "keywords": [ "angularjs", "javascript", @@ -11,23 +14,41 @@ "dist/angular-leaflet-directive.js" ], "dependencies": { - "angular": "1.2.x", - "leaflet-dist": "0.7.x" + "angular": "1.x", + "leaflet": "0.7.x" }, "devDependencies": { - "jquery": "2.1.x", - "bootstrap": "3.1.x", - "angular-route": "1.2.x", - "angular-animate": "1.2.x", - "angular-mocks": "1.2.x", - "leaflet.markerclusterer": "0.4", - "leaflet.draw": "0.2.2", - "Leaflet.label": "0.2.1", + "jquery": "*", + "semantic-ui": "*", + "bootstrap": "*", + "prism": "*", + "angular-route": "1.x", + "angular-animate": "1.x", + "angular-mocks": "1.x", + "leaflet.markercluster": "*", + "leaflet.draw": "*", + "Leaflet.label": "*", "leaflet-tilelayer-geojson": "*", + "Leaflet.utfgrid": "danzel/Leaflet.utfgrid", "Leaflet.awesome-markers": "*", + "leaflet-providers": "*", + "leaflet.vector-markers": "*", "webgl-heatmap-leaflet": "*", - "leaflet-plugins": "1.0.1", - "esri-leaflet": "0.0.1-beta.4" + "leaflet-plugins": "*", + "esri-leaflet": "*", + "proj4": "*", + "font-awesome": "*", + "proj4leaflet": "*", + "Leaflet.MakiMarkers": "*", + "Leaflet.heat": "https://github.com/Leaflet/Leaflet.heat/archive/gh-pages.tar.gz", + "Leaflet.ExtraMarkers": "https://github.com/coryasilva/Leaflet.ExtraMarkers/archive/v1.0.1.tar.gz", + "Leaflet.fullscreen": "http://github.com/Leaflet/Leaflet.fullscreen/archive/v0.0.4.tar.gz", + "Leaflet.PolylineDecorator": "bbecquet/Leaflet.PolylineDecorator", + "ionrangeslider": "*", + "leaflet-minimap": "*", + "esri-leaflet-clustered-feature-layer": "~1.0.x", + "esri-leaflet-heatmap-feature-layer": "~1.0.x", + "leaflet-search": "*" }, "ignore": [ "**/.*", @@ -40,15 +61,13 @@ "package.json", "bower.json" ], - "homepage": "https://github.com/tombatossals/angular-leaflet-directive", - "version": "0.7.7", - "_release": "0.7.7", + "_release": "0.10.0", "_resolution": { "type": "version", - "tag": "v0.7.7", - "commit": "f2b426b3daf3a3753bb76a3526956f14818f358c" + "tag": "v0.10.0", + "commit": "15323bc8c3bad3f2dedafbcfebb6772dc0813cfb" }, - "_source": "git://github.com/tombatossals/angular-leaflet-directive.git", - "_target": "~0.7.7", + "_source": "https://github.com/tombatossals/angular-leaflet-directive.git", + "_target": "~0.10.0", "_originalSource": "angular-leaflet-directive" } \ No newline at end of file diff --git a/app/bower_components/angular-leaflet-directive/LICENSE b/app/bower_components/angular-leaflet-directive/LICENSE index 6896b53..9636811 100644 --- a/app/bower_components/angular-leaflet-directive/LICENSE +++ b/app/bower_components/angular-leaflet-directive/LICENSE @@ -1,6 +1,6 @@ The MIT License -Copyright (c) 2012-2013 https://github.com/tombatossals/angular-leaflet-directive +Copyright (c) https://github.com/tombatossals/angular-leaflet-directive Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/app/bower_components/angular-leaflet-directive/bower.json b/app/bower_components/angular-leaflet-directive/bower.json new file mode 100644 index 0000000..3c3c712 --- /dev/null +++ b/app/bower_components/angular-leaflet-directive/bower.json @@ -0,0 +1,64 @@ +{ + "name": "angular-leaflet-directive", + "author": "https://github.com/tombatossals/angular-leaflet-directive/graphs/contributors", + "description": "angular-leaflet-directive - An AngularJS directive to easily interact with Leaflet maps", + "version": "0.9.0", + "homepage": "http://tombatossals.github.io/angular-leaflet-directive/", + "keywords": [ + "angularjs", + "javascript", + "directive", + "leaflet" + ], + "main": [ + "dist/angular-leaflet-directive.js" + ], + "dependencies": { + "angular": "1.x", + "leaflet": "0.7.x" + }, + "devDependencies": { + "jquery": "*", + "semantic-ui": "*", + "bootstrap": "*", + "prism": "*", + "angular-route": "1.x", + "angular-animate": "1.x", + "angular-mocks": "1.x", + "leaflet.markercluster": "*", + "leaflet.draw": "*", + "Leaflet.label": "*", + "leaflet-tilelayer-geojson": "*", + "Leaflet.utfgrid": "danzel/Leaflet.utfgrid", + "Leaflet.awesome-markers": "*", + "leaflet-providers": "*", + "leaflet.vector-markers": "*", + "webgl-heatmap-leaflet": "*", + "leaflet-plugins": "*", + "esri-leaflet": "*", + "proj4": "*", + "font-awesome": "*", + "proj4leaflet": "*", + "Leaflet.MakiMarkers": "*", + "Leaflet.heat": "https://github.com/Leaflet/Leaflet.heat/archive/gh-pages.tar.gz", + "Leaflet.ExtraMarkers": "https://github.com/coryasilva/Leaflet.ExtraMarkers/archive/v1.0.1.tar.gz", + "Leaflet.fullscreen": "http://github.com/Leaflet/Leaflet.fullscreen/archive/v0.0.4.tar.gz", + "Leaflet.PolylineDecorator": "bbecquet/Leaflet.PolylineDecorator", + "ionrangeslider": "*", + "leaflet-minimap": "*", + "esri-leaflet-clustered-feature-layer": "~1.0.x", + "esri-leaflet-heatmap-feature-layer": "~1.0.x", + "leaflet-search": "*" + }, + "ignore": [ + "**/.*", + "src", + "doc", + "examples", + "test", + "*.md", + "Gruntfile.js", + "package.json", + "bower.json" + ] +} diff --git a/app/bower_components/angular-leaflet-directive/coffeelint.json b/app/bower_components/angular-leaflet-directive/coffeelint.json new file mode 100644 index 0000000..761e6d3 --- /dev/null +++ b/app/bower_components/angular-leaflet-directive/coffeelint.json @@ -0,0 +1,114 @@ +{ + "coffeescript_error": { + "level": "error" + }, + "arrow_spacing": { + "name": "arrow_spacing", + "level": "warn" + }, + "no_tabs": { + "name": "no_tabs", + "level": "error" + }, + "no_trailing_whitespace": { + "name": "no_trailing_whitespace", + "level": "warn", + "allowed_in_comments": false, + "allowed_in_empty_lines": true + }, + "max_line_length": { + "name": "max_line_length", + "value": 125, + "level": "warn", + "limitComments": true + }, + "line_endings": { + "name": "line_endings", + "level": "ignore", + "value": "unix" + }, + "no_trailing_semicolons": { + "name": "no_trailing_semicolons", + "level": "error" + }, + "indentation": { + "name": "indentation", + "value": 4, + "level": "error" + }, + "camel_case_classes": { + "name": "camel_case_classes", + "level": "error" + }, + "colon_assignment_spacing": { + "name": "colon_assignment_spacing", + "level": "warn", + "spacing": { + "left": 0, + "right": 1 + } + }, + "no_implicit_braces": { + "name": "no_implicit_braces", + "level": "ignore", + "strict": true + }, + "no_plusplus": { + "name": "no_plusplus", + "level": "ignore" + }, + "no_throwing_strings": { + "name": "no_throwing_strings", + "level": "error" + }, + "no_backticks": { + "name": "no_backticks", + "level": "error" + }, + "no_implicit_parens": { + "name": "no_implicit_parens", + "level": "ignore" + }, + "no_empty_param_list": { + "name": "no_empty_param_list", + "level": "warn" + }, + "no_stand_alone_at": { + "name": "no_stand_alone_at", + "level": "ignore" + }, + "space_operators": { + "name": "space_operators", + "level": "warn" + }, + "duplicate_key": { + "name": "duplicate_key", + "level": "error" + }, + "empty_constructor_needs_parens": { + "name": "empty_constructor_needs_parens", + "level": "ignore" + }, + "cyclomatic_complexity": { + "name": "cyclomatic_complexity", + "value": 10, + "level": "ignore" + }, + "newlines_after_classes": { + "name": "newlines_after_classes", + "value": 3, + "level": "ignore" + }, + "no_unnecessary_fat_arrows": { + "name": "no_unnecessary_fat_arrows", + "level": "warn" + }, + "missing_fat_arrows": { + "name": "missing_fat_arrows", + "level": "ignore" + }, + "non_empty_constructor_needs_parens": { + "name": "non_empty_constructor_needs_parens", + "level": "ignore" + } +} diff --git a/app/bower_components/angular-leaflet-directive/dist/angular-leaflet-directive.js b/app/bower_components/angular-leaflet-directive/dist/angular-leaflet-directive.js index e143826..b4e92c9 100644 --- a/app/bower_components/angular-leaflet-directive/dist/angular-leaflet-directive.js +++ b/app/bower_components/angular-leaflet-directive/dist/angular-leaflet-directive.js @@ -1,178 +1,3450 @@ -(function () { - 'use strict'; - angular.module('leaflet-directive', []).directive('leaflet', [ - '$q', - 'leafletData', - 'leafletMapDefaults', - 'leafletHelpers', - 'leafletEvents', - function ($q, leafletData, leafletMapDefaults, leafletHelpers, leafletEvents) { - var _leafletMap; - return { - restrict: 'EA', - replace: true, - scope: { - center: '=center', - defaults: '=defaults', - maxbounds: '=maxbounds', - bounds: '=bounds', - markers: '=markers', - legend: '=legend', - geojson: '=geojson', - paths: '=paths', - tiles: '=tiles', - layers: '=layers', - controls: '=controls', - eventBroadcast: '=eventBroadcast' - }, - template: '
        ', - controller: [ - '$scope', - function ($scope) { - _leafletMap = $q.defer(); - this.getMap = function () { - return _leafletMap.promise; - }; - this.getLeafletScope = function () { - return $scope; - }; - } - ], - link: function (scope, element, attrs) { - var isDefined = leafletHelpers.isDefined, defaults = leafletMapDefaults.setDefaults(scope.defaults, attrs.id), genDispatchMapEvent = leafletEvents.genDispatchMapEvent, mapEvents = leafletEvents.getAvailableMapEvents(); - // Set width and height if they are defined - if (isDefined(attrs.width)) { - if (isNaN(attrs.width)) { - element.css('width', attrs.width); - } else { - element.css('width', attrs.width + 'px'); - } - } - if (isDefined(attrs.height)) { - if (isNaN(attrs.height)) { - element.css('height', attrs.height); - } else { - element.css('height', attrs.height + 'px'); - } - } - // Create the Leaflet Map Object with the options - var map = new L.Map(element[0], leafletMapDefaults.getMapCreationDefaults(attrs.id)); - _leafletMap.resolve(map); - if (!isDefined(attrs.center)) { - map.setView([ - defaults.center.lat, - defaults.center.lng - ], defaults.center.zoom); - } - // If no layers nor tiles defined, set the default tileLayer - if (!isDefined(attrs.tiles) && !isDefined(attrs.layers)) { - var tileLayerObj = L.tileLayer(defaults.tileLayer, defaults.tileLayerOptions); - tileLayerObj.addTo(map); - leafletData.setTiles(tileLayerObj, attrs.id); - } - // Set zoom control configuration - if (isDefined(map.zoomControl) && isDefined(defaults.zoomControlPosition)) { - map.zoomControl.setPosition(defaults.zoomControlPosition); - } - if (isDefined(map.zoomControl) && defaults.zoomControl === false) { - map.zoomControl.removeFrom(map); - } - if (isDefined(map.zoomsliderControl) && isDefined(defaults.zoomsliderControl) && defaults.zoomsliderControl === false) { - map.zoomsliderControl.removeFrom(map); - } - // if no event-broadcast attribute, all events are broadcasted - if (!isDefined(attrs.eventBroadcast)) { - var logic = 'broadcast'; - for (var i = 0; i < mapEvents.length; i++) { - var eventName = mapEvents[i]; - map.on(eventName, genDispatchMapEvent(scope, eventName, logic), { eventName: eventName }); - } - } - // Resolve the map object to the promises - map.whenReady(function () { - leafletData.setMap(map, attrs.id); - }); - scope.$on('$destroy', function () { - leafletData.unresolveMap(attrs.id); - }); +/**! + * The MIT License + * + * Copyright (c) 2013 the angular-leaflet-directive Team, http://tombatossals.github.io/angular-leaflet-directive + * + * 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. + * + * angular-leaflet-directive + * https://github.com/tombatossals/angular-leaflet-directive + * + * @authors https://github.com/tombatossals/angular-leaflet-directive/graphs/contributors + */ + +/*! +* angular-leaflet-directive 2015-11-06 +* angular-leaflet-directive - An AngularJS directive to easily interact with Leaflet maps +* git: https://github.com/tombatossals/angular-leaflet-directive +*/ +(function(angular){ +'use strict'; +angular.module('leaflet-directive', []).directive('leaflet', ["$q", "leafletData", "leafletMapDefaults", "leafletHelpers", "leafletMapEvents", function($q, leafletData, leafletMapDefaults, leafletHelpers, leafletMapEvents) { + return { + restrict: 'EA', + replace: true, + scope: { + center: '=', + lfCenter: '=', + defaults: '=', + maxbounds: '=', + bounds: '=', + markers: '=', + legend: '=', + geojson: '=', + paths: '=', + tiles: '=', + layers: '=', + controls: '=', + decorations: '=', + eventBroadcast: '=', + markersWatchOptions: '=', + geojsonWatchOptions: '=', + }, + transclude: true, + template: '
        ', + controller: ["$scope", function($scope) { + this._leafletMap = $q.defer(); + this.getMap = function() { + return this._leafletMap.promise; + }; + + this.getLeafletScope = function() { + return $scope; + }; + }], + + link: function(scope, element, attrs, ctrl) { + var isDefined = leafletHelpers.isDefined; + var defaults = leafletMapDefaults.setDefaults(scope.defaults, attrs.id); + var mapEvents = leafletMapEvents.getAvailableMapEvents(); + var addEvents = leafletMapEvents.addEvents; + + scope.mapId = attrs.id; + leafletData.setDirectiveControls({}, attrs.id); + + // Set width and height utility functions + function updateWidth() { + if (isNaN(attrs.width)) { + element.css('width', attrs.width); + } else { + element.css('width', attrs.width + 'px'); } + } + + function updateHeight() { + if (isNaN(attrs.height)) { + element.css('height', attrs.height); + } else { + element.css('height', attrs.height + 'px'); + } + } + + // If the width attribute defined update css + // Then watch if bound property changes and update css + if (isDefined(attrs.width)) { + updateWidth(); + + scope.$watch( + function() { + return element[0].getAttribute('width'); + }, + + function() { + updateWidth(); + map.invalidateSize(); + }); + } + + // If the height attribute defined update css + // Then watch if bound property changes and update css + if (isDefined(attrs.height)) { + updateHeight(); + + scope.$watch( + function() { + return element[0].getAttribute('height'); + }, + + function() { + updateHeight(); + map.invalidateSize(); + }); + } + + // Create the Leaflet Map Object with the options + var map = new L.Map(element[0], leafletMapDefaults.getMapCreationDefaults(attrs.id)); + ctrl._leafletMap.resolve(map); + + if (!isDefined(attrs.center) && !isDefined(attrs.lfCenter)) { + map.setView([defaults.center.lat, defaults.center.lng], defaults.center.zoom); + } + + // If no layers nor tiles defined, set the default tileLayer + if (!isDefined(attrs.tiles) && (!isDefined(attrs.layers))) { + var tileLayerObj = L.tileLayer(defaults.tileLayer, defaults.tileLayerOptions); + tileLayerObj.addTo(map); + leafletData.setTiles(tileLayerObj, attrs.id); + } + + // Set zoom control configuration + if (isDefined(map.zoomControl) && + isDefined(defaults.zoomControlPosition)) { + map.zoomControl.setPosition(defaults.zoomControlPosition); + } + + if (isDefined(map.zoomControl) && defaults.zoomControl === false) { + map.zoomControl.removeFrom(map); + } + + if (isDefined(map.zoomsliderControl) && + isDefined(defaults.zoomsliderControl) && + defaults.zoomsliderControl === false) { + map.zoomsliderControl.removeFrom(map); + } + + // if no event-broadcast attribute, all events are broadcasted + if (!isDefined(attrs.eventBroadcast)) { + var logic = 'broadcast'; + addEvents(map, mapEvents, 'eventName', scope, logic); + } + + // Resolve the map object to the promises + map.whenReady(function() { + leafletData.setMap(map, attrs.id); + }); + + scope.$on('$destroy', function() { + leafletMapDefaults.reset(); + map.remove(); + leafletData.unresolveMap(attrs.id); + }); + + //Handle request to invalidate the map size + //Up scope using $scope.$emit('invalidateSize') + //Down scope using $scope.$broadcast('invalidateSize') + scope.$on('invalidateSize', function() { + map.invalidateSize(); + }); + }, + }; +}]); + +angular.module('leaflet-directive').factory('leafletBoundsHelpers', ["$log", "leafletHelpers", function($log, leafletHelpers) { + + var isArray = leafletHelpers.isArray; + var isNumber = leafletHelpers.isNumber; + var isFunction = leafletHelpers.isFunction; + var isDefined = leafletHelpers.isDefined; + + function _isValidBounds(bounds) { + return angular.isDefined(bounds) && angular.isDefined(bounds.southWest) && + angular.isDefined(bounds.northEast) && angular.isNumber(bounds.southWest.lat) && + angular.isNumber(bounds.southWest.lng) && angular.isNumber(bounds.northEast.lat) && + angular.isNumber(bounds.northEast.lng); + } + + return { + createLeafletBounds: function(bounds) { + if (_isValidBounds(bounds)) { + return L.latLngBounds([bounds.southWest.lat, bounds.southWest.lng], + [bounds.northEast.lat, bounds.northEast.lng]); + } + }, + + isValidBounds: _isValidBounds, + + createBoundsFromArray: function(boundsArray) { + if (!(isArray(boundsArray) && boundsArray.length === 2 && + isArray(boundsArray[0]) && isArray(boundsArray[1]) && + boundsArray[0].length === 2 && boundsArray[1].length === 2 && + isNumber(boundsArray[0][0]) && isNumber(boundsArray[0][1]) && + isNumber(boundsArray[1][0]) && isNumber(boundsArray[1][1]))) { + $log.error('[AngularJS - Leaflet] The bounds array is not valid.'); + return; + } + + return { + northEast: { + lat: boundsArray[0][0], + lng: boundsArray[0][1], + }, + southWest: { + lat: boundsArray[1][0], + lng: boundsArray[1][1], + }, + }; + }, + + createBoundsFromLeaflet: function(lfBounds) { + if (!(isDefined(lfBounds) && isFunction(lfBounds.getNorthEast) && isFunction(lfBounds.getSouthWest))) { + $log.error('[AngularJS - Leaflet] The leaflet bounds is not valid object.'); + return; + } + + var northEast = lfBounds.getNorthEast(); + var southWest = lfBounds.getSouthWest(); + + return { + northEast: { + lat: northEast.lat, + lng: northEast.lng, + }, + southWest: { + lat: southWest.lat, + lng: southWest.lng, + }, + }; + }, + }; +}]); + +angular.module('leaflet-directive').factory('leafletControlHelpers', ["$rootScope", "$log", "leafletHelpers", "leafletLayerHelpers", "leafletMapDefaults", function($rootScope, $log, leafletHelpers, leafletLayerHelpers, leafletMapDefaults) { + var isDefined = leafletHelpers.isDefined; + var isObject = leafletHelpers.isObject; + var createLayer = leafletLayerHelpers.createLayer; + var _controls = {}; + var errorHeader = leafletHelpers.errorHeader + ' [Controls] '; + + var _controlLayersMustBeVisible = function(baselayers, overlays, mapId) { + var defaults = leafletMapDefaults.getDefaults(mapId); + if (!defaults.controls.layers.visible) { + return false; + } + + var atLeastOneControlItemMustBeShown = false; + + if (isObject(baselayers)) { + Object.keys(baselayers).forEach(function(key) { + var layer = baselayers[key]; + if (!isDefined(layer.layerOptions) || layer.layerOptions.showOnSelector !== false) { + atLeastOneControlItemMustBeShown = true; + } + }); + } + + if (isObject(overlays)) { + Object.keys(overlays).forEach(function(key) { + var layer = overlays[key]; + if (!isDefined(layer.layerParams) || layer.layerParams.showOnSelector !== false) { + atLeastOneControlItemMustBeShown = true; + } + }); + } + + return atLeastOneControlItemMustBeShown; + }; + + var _createLayersControl = function(mapId) { + var defaults = leafletMapDefaults.getDefaults(mapId); + var controlOptions = { + collapsed: defaults.controls.layers.collapsed, + position: defaults.controls.layers.position, + autoZIndex: false, + }; + + angular.extend(controlOptions, defaults.controls.layers.options); + + var control; + if (defaults.controls.layers && isDefined(defaults.controls.layers.control)) { + control = defaults.controls.layers.control.apply(this, [[], [], controlOptions]); + } else { + control = new L.control.layers([], [], controlOptions); + } + + return control; + }; + + var controlTypes = { + draw: { + isPluginLoaded: function() { + if (!angular.isDefined(L.Control.Draw)) { + $log.error(errorHeader + ' Draw plugin is not loaded.'); + return false; + } + + return true; + }, + + checkValidParams: function(/* params */) { + return true; + }, + + createControl: function(params) { + return new L.Control.Draw(params); + }, + }, + scale: { + isPluginLoaded: function() { + return true; + }, + + checkValidParams: function(/* params */) { + return true; + }, + + createControl: function(params) { + return new L.control.scale(params); + }, + }, + fullscreen: { + isPluginLoaded: function() { + if (!angular.isDefined(L.Control.Fullscreen)) { + $log.error(errorHeader + ' Fullscreen plugin is not loaded.'); + return false; + } + + return true; + }, + + checkValidParams: function(/* params */) { + return true; + }, + + createControl: function(params) { + return new L.Control.Fullscreen(params); + }, + }, + search: { + isPluginLoaded: function() { + if (!angular.isDefined(L.Control.Search)) { + $log.error(errorHeader + ' Search plugin is not loaded.'); + return false; + } + + return true; + }, + + checkValidParams: function(/* params */) { + return true; + }, + + createControl: function(params) { + return new L.Control.Search(params); + }, + }, + custom: {}, + minimap: { + isPluginLoaded: function() { + if (!angular.isDefined(L.Control.MiniMap)) { + $log.error(errorHeader + ' Minimap plugin is not loaded.'); + return false; + } + + return true; + }, + + checkValidParams: function(params) { + if (!isDefined(params.layer)) { + $log.warn(errorHeader + ' minimap "layer" option should be defined.'); + return false; + } + + return true; + }, + + createControl: function(params) { + var layer = createLayer(params.layer); + + if (!isDefined(layer)) { + $log.warn(errorHeader + ' minimap control "layer" could not be created.'); + return; + } + + return new L.Control.MiniMap(layer, params); + }, + }, + }; + + return { + layersControlMustBeVisible: _controlLayersMustBeVisible, + + isValidControlType: function(type) { + return Object.keys(controlTypes).indexOf(type) !== -1; + }, + + createControl: function(type, params) { + if (!controlTypes[type].checkValidParams(params)) { + return; + } + + return controlTypes[type].createControl(params); + }, + + updateLayersControl: function(map, mapId, loaded, baselayers, overlays, leafletLayers) { + var i; + var _layersControl = _controls[mapId]; + var mustBeLoaded = _controlLayersMustBeVisible(baselayers, overlays, mapId); + + if (isDefined(_layersControl) && loaded) { + for (i in leafletLayers.baselayers) { + _layersControl.removeLayer(leafletLayers.baselayers[i]); + } + + for (i in leafletLayers.overlays) { + _layersControl.removeLayer(leafletLayers.overlays[i]); + } + + map.removeControl(_layersControl); + delete _controls[mapId]; + } + + if (mustBeLoaded) { + _layersControl = _createLayersControl(mapId); + _controls[mapId] = _layersControl; + for (i in baselayers) { + var hideOnSelector = isDefined(baselayers[i].layerOptions) && + baselayers[i].layerOptions.showOnSelector === false; + if (!hideOnSelector && isDefined(leafletLayers.baselayers[i])) { + _layersControl.addBaseLayer(leafletLayers.baselayers[i], baselayers[i].name); + } + } + + for (i in overlays) { + var hideOverlayOnSelector = isDefined(overlays[i].layerParams) && + overlays[i].layerParams.showOnSelector === false; + if (!hideOverlayOnSelector && isDefined(leafletLayers.overlays[i])) { + _layersControl.addOverlay(leafletLayers.overlays[i], overlays[i].name); + } + } + + map.addControl(_layersControl); + } + + return mustBeLoaded; + }, + }; +}]); + +angular.module('leaflet-directive').service('leafletData', ["$log", "$q", "leafletHelpers", function($log, $q, leafletHelpers) { + var getDefer = leafletHelpers.getDefer, + getUnresolvedDefer = leafletHelpers.getUnresolvedDefer, + setResolvedDefer = leafletHelpers.setResolvedDefer; + + var _private = {}; + var self = this; + + var upperFirst = function(string) { + return string.charAt(0).toUpperCase() + string.slice(1); + }; + + var _privateItems = [ + 'map', + 'tiles', + 'layers', + 'paths', + 'markers', + 'geoJSON', + 'UTFGrid', //odd ball on naming convention keeping to not break + 'decorations', + 'directiveControls',]; + + //init + _privateItems.forEach(function(itemName) { + _private[itemName] = {}; + }); + + this.unresolveMap = function(scopeId) { + var id = leafletHelpers.obtainEffectiveMapId(_private.map, scopeId); + _privateItems.forEach(function(itemName) { + _private[itemName][id] = undefined; + }); + }; + + //int repetitive stuff (get and sets) + _privateItems.forEach(function(itemName) { + var name = upperFirst(itemName); + self['set' + name] = function(lObject, scopeId) { + var defer = getUnresolvedDefer(_private[itemName], scopeId); + defer.resolve(lObject); + setResolvedDefer(_private[itemName], scopeId); + }; + + self['get' + name] = function(scopeId) { + var defer = getDefer(_private[itemName], scopeId); + return defer.promise; + }; + }); +}]); + +angular.module('leaflet-directive') +.service('leafletDirectiveControlsHelpers', ["$log", "leafletData", "leafletHelpers", function($log, leafletData, leafletHelpers) { + var _isDefined = leafletHelpers.isDefined; + var _isString = leafletHelpers.isString; + var _isObject = leafletHelpers.isObject; + var _mainErrorHeader = leafletHelpers.errorHeader; + + var _errorHeader = _mainErrorHeader + '[leafletDirectiveControlsHelpers'; + + var _extend = function(id, thingToAddName, createFn, cleanFn) { + var _fnHeader = _errorHeader + '.extend] '; + var extender = {}; + if (!_isDefined(thingToAddName)) { + $log.error(_fnHeader + 'thingToAddName cannot be undefined'); + return; + } + + if (_isString(thingToAddName) && _isDefined(createFn) && _isDefined(cleanFn)) { + extender[thingToAddName] = { + create: createFn, + clean: cleanFn, + }; + } else if (_isObject(thingToAddName) && !_isDefined(createFn) && !_isDefined(cleanFn)) { + extender = thingToAddName; + } else { + $log.error(_fnHeader + 'incorrect arguments'); + return; + } + + //add external control to create / destroy markers without a watch + leafletData.getDirectiveControls().then(function(controls) { + angular.extend(controls, extender); + leafletData.setDirectiveControls(controls, id); + }); + }; + + return { + extend: _extend, + }; +}]); + +angular.module('leaflet-directive') +.service('leafletGeoJsonHelpers', ["leafletHelpers", "leafletIterators", function(leafletHelpers, leafletIterators) { + var lHlp = leafletHelpers; + var lIt = leafletIterators; + var Point = function(lat, lng) { + this.lat = lat; + this.lng = lng; + return this; + }; + + var _getLat = function(value) { + if (Array.isArray(value) && value.length === 2) { + return value[1]; + } else if (lHlp.isDefined(value.type) && value.type === 'Point') { + return +value.coordinates[1]; + } else { + return +value.lat; + } + }; + + var _getLng = function(value) { + if (Array.isArray(value) && value.length === 2) { + return value[0]; + } else if (lHlp.isDefined(value.type) && value.type === 'Point') { + return +value.coordinates[0]; + } else { + return +value.lng; + } + }; + + var _validateCoords = function(coords) { + if (lHlp.isUndefined(coords)) { + return false; + } + + if (lHlp.isArray(coords)) { + if (coords.length === 2 && lHlp.isNumber(coords[0]) && lHlp.isNumber(coords[1])) { + return true; + } + } else if (lHlp.isDefined(coords.type)) { + if ( + coords.type === 'Point' && lHlp.isArray(coords.coordinates) && + coords.coordinates.length === 2 && + lHlp.isNumber(coords.coordinates[0]) && + lHlp.isNumber(coords.coordinates[1])) { + return true; + } + } + + var ret = lIt.all(['lat', 'lng'], function(pos) { + return lHlp.isDefined(coords[pos]) && lHlp.isNumber(coords[pos]); + }); + + return ret; + }; + + var _getCoords = function(value) { + if (!value || !_validateCoords(value)) { + return; + } + + var p = null; + if (Array.isArray(value) && value.length === 2) { + p = new Point(value[1], value[0]); + } else if (lHlp.isDefined(value.type) && value.type === 'Point') { + p = new Point(value.coordinates[1], value.coordinates[0]); + } else { + return value; + } + + //note angular.merge is avail in angular 1.4.X we might want to fill it here + return angular.extend(value, p);//tap on lat, lng if it doesnt exist + }; + + return { + getLat: _getLat, + getLng: _getLng, + validateCoords: _validateCoords, + getCoords: _getCoords, + }; +}]); + +angular.module('leaflet-directive').service('leafletHelpers', ["$q", "$log", function($q, $log) { + var _errorHeader = '[AngularJS - Leaflet] '; + var _copy = angular.copy; + var _clone = _copy; + /* + For parsing paths to a field in an object + + Example: + var obj = { + bike:{ + 1: 'hi' + 2: 'foo' + } + }; + _getObjectValue(obj,"bike.1") returns 'hi' + this is getPath in ui-gmap + */ + var _getObjectValue = function(object, pathStr) { + var obj; + if (!object || !angular.isObject(object)) + return; + + //if the key is not a sting then we already have the value + if ((pathStr === null) || !angular.isString(pathStr)) { + return pathStr; + } + + obj = object; + pathStr.split('.').forEach(function(value) { + if (obj) { + obj = obj[value]; + } + }); + + return obj; + }; + + /* + Object Array Notation + _getObjectArrayPath("bike.one.two") + returns: + 'bike["one"]["two"]' + */ + var _getObjectArrayPath = function(pathStr) { + return pathStr.split('.').reduce(function(previous, current) { + return previous + '["' + current + '"]'; + }); + }; + + /* Object Dot Notation + _getObjectPath(["bike","one","two"]) + returns: + "bike.one.two" + */ + var _getObjectDotPath = function(arrayOfStrings) { + return arrayOfStrings.reduce(function(previous, current) { + return previous + '.' + current; + }); + }; + + function _obtainEffectiveMapId(d, mapId) { + var id; + var i; + if (!angular.isDefined(mapId)) { + if (Object.keys(d).length === 0) { + id = 'main'; + } else if (Object.keys(d).length >= 1) { + for (i in d) { + if (d.hasOwnProperty(i)) { + id = i; + } + } + } else { + $log.error(_errorHeader + '- You have more than 1 map on the DOM, you must provide the map ID to the leafletData.getXXX call'); + } + } else { + id = mapId; + } + + return id; + } + + function _getUnresolvedDefer(d, mapId) { + var id = _obtainEffectiveMapId(d, mapId); + var defer; + + if (!angular.isDefined(d[id]) || d[id].resolvedDefer === true) { + defer = $q.defer(); + d[id] = { + defer: defer, + resolvedDefer: false, + }; + } else { + defer = d[id].defer; + } + + return defer; + } + + var _isDefined = function(value) { + return angular.isDefined(value) && value !== null; + }; + + var _isUndefined = function(value) { + return !_isDefined(value); + }; + + // BEGIN DIRECT PORT FROM AngularJS code base + + var SPECIAL_CHARS_REGEXP = /([\:\-\_]+(.))/g; + + var MOZ_HACK_REGEXP = /^moz([A-Z])/; + + var PREFIX_REGEXP = /^((?:x|data)[\:\-_])/i; + + /** + Converts snake_case to camelCase. + Also there is special case for Moz prefix starting with upper case letter. + @param name Name to normalize + */ + + var camelCase = function(name) { + return name.replace(SPECIAL_CHARS_REGEXP, function(_, separator, letter, offset) { + if (offset) { + return letter.toUpperCase(); + } else { + return letter; + } + }).replace(MOZ_HACK_REGEXP, 'Moz$1'); + }; + + /** + Converts all accepted directives format into proper directive name. + @param name Name to normalize + */ + + var directiveNormalize = function(name) { + return camelCase(name.replace(PREFIX_REGEXP, '')); + }; + + // END AngularJS port + + return { + camelCase: camelCase, + directiveNormalize: directiveNormalize, + copy:_copy, + clone:_clone, + errorHeader: _errorHeader, + getObjectValue: _getObjectValue, + getObjectArrayPath:_getObjectArrayPath, + getObjectDotPath: _getObjectDotPath, + defaultTo: function(val, _default) { + return _isDefined(val) ? val : _default; + }, + + //mainly for checking attributes of directives lets keep this minimal (on what we accept) + isTruthy: function(val) { + return val === 'true' || val === true; + }, + + //Determine if a reference is {} + isEmpty: function(value) { + return Object.keys(value).length === 0; + }, + + //Determine if a reference is undefined or {} + isUndefinedOrEmpty: function(value) { + return (angular.isUndefined(value) || value === null) || Object.keys(value).length === 0; + }, + + // Determine if a reference is defined + isDefined: _isDefined, + isUndefined:_isUndefined, + isNumber: angular.isNumber, + isString: angular.isString, + isArray: angular.isArray, + isObject: angular.isObject, + isFunction: angular.isFunction, + equals: angular.equals, + + isValidCenter: function(center) { + return angular.isDefined(center) && angular.isNumber(center.lat) && + angular.isNumber(center.lng) && angular.isNumber(center.zoom); + }, + + isValidPoint: function(point) { + if (!angular.isDefined(point)) { + return false; + } + + if (angular.isArray(point)) { + return point.length === 2 && angular.isNumber(point[0]) && angular.isNumber(point[1]); + } + + return angular.isNumber(point.lat) && angular.isNumber(point.lng); + }, + + isSameCenterOnMap: function(centerModel, map) { + var mapCenter = map.getCenter(); + var zoom = map.getZoom(); + if (centerModel.lat && centerModel.lng && + mapCenter.lat.toFixed(4) === centerModel.lat.toFixed(4) && + mapCenter.lng.toFixed(4) === centerModel.lng.toFixed(4) && + zoom === centerModel.zoom) { + return true; + } + + return false; + }, + + safeApply: function($scope, fn) { + var phase = $scope.$root.$$phase; + if (phase === '$apply' || phase === '$digest') { + $scope.$eval(fn); + } else { + $scope.$evalAsync(fn); + } + }, + + obtainEffectiveMapId: _obtainEffectiveMapId, + + getDefer: function(d, mapId) { + var id = _obtainEffectiveMapId(d, mapId); + var defer; + if (!angular.isDefined(d[id]) || d[id].resolvedDefer === false) { + defer = _getUnresolvedDefer(d, mapId); + } else { + defer = d[id].defer; + } + + return defer; + }, + + getUnresolvedDefer: _getUnresolvedDefer, + + setResolvedDefer: function(d, mapId) { + var id = _obtainEffectiveMapId(d, mapId); + d[id].resolvedDefer = true; + }, + + rangeIsSupported: function() { + var testrange = document.createElement('input'); + testrange.setAttribute('type', 'range'); + return testrange.type === 'range'; + }, + + FullScreenControlPlugin: { + isLoaded: function() { + return angular.isDefined(L.Control.Fullscreen); + }, + }, + + MiniMapControlPlugin: { + isLoaded: function() { + return angular.isDefined(L.Control.MiniMap); + }, + }, + + AwesomeMarkersPlugin: { + isLoaded: function() { + return angular.isDefined(L.AwesomeMarkers) && angular.isDefined(L.AwesomeMarkers.Icon); + }, + + is: function(icon) { + if (this.isLoaded()) { + return icon instanceof L.AwesomeMarkers.Icon; + } else { + return false; + } + }, + + equal: function(iconA, iconB) { + if (!this.isLoaded()) { + return false; + } + + if (this.is(iconA)) { + return angular.equals(iconA, iconB); + } else { + return false; + } + }, + }, + + VectorMarkersPlugin: { + isLoaded: function() { + return angular.isDefined(L.VectorMarkers) && angular.isDefined(L.VectorMarkers.Icon); + }, + + is: function(icon) { + if (this.isLoaded()) { + return icon instanceof L.VectorMarkers.Icon; + } else { + return false; + } + }, + + equal: function(iconA, iconB) { + if (!this.isLoaded()) { + return false; + } + + if (this.is(iconA)) { + return angular.equals(iconA, iconB); + } else { + return false; + } + }, + }, + + DomMarkersPlugin: { + isLoaded: function() { + if (angular.isDefined(L.DomMarkers) && angular.isDefined(L.DomMarkers.Icon)) { + return true; + } else { + return false; + } + }, + + is: function(icon) { + if (this.isLoaded()) { + return icon instanceof L.DomMarkers.Icon; + } else { + return false; + } + }, + + equal: function(iconA, iconB) { + if (!this.isLoaded()) { + return false; + } + + if (this.is(iconA)) { + return angular.equals(iconA, iconB); + } else { + return false; + } + }, + }, + + PolylineDecoratorPlugin: { + isLoaded: function() { + if (angular.isDefined(L.PolylineDecorator)) { + return true; + } else { + return false; + } + }, + + is: function(decoration) { + if (this.isLoaded()) { + return decoration instanceof L.PolylineDecorator; + } else { + return false; + } + }, + + equal: function(decorationA, decorationB) { + if (!this.isLoaded()) { + return false; + } + + if (this.is(decorationA)) { + return angular.equals(decorationA, decorationB); + } else { + return false; + } + }, + }, + + MakiMarkersPlugin: { + isLoaded: function() { + if (angular.isDefined(L.MakiMarkers) && angular.isDefined(L.MakiMarkers.Icon)) { + return true; + } else { + return false; + } + }, + + is: function(icon) { + if (this.isLoaded()) { + return icon instanceof L.MakiMarkers.Icon; + } else { + return false; + } + }, + + equal: function(iconA, iconB) { + if (!this.isLoaded()) { + return false; + } + + if (this.is(iconA)) { + return angular.equals(iconA, iconB); + } else { + return false; + } + }, + }, + ExtraMarkersPlugin: { + isLoaded: function() { + if (angular.isDefined(L.ExtraMarkers) && angular.isDefined(L.ExtraMarkers.Icon)) { + return true; + } else { + return false; + } + }, + + is: function(icon) { + if (this.isLoaded()) { + return icon instanceof L.ExtraMarkers.Icon; + } else { + return false; + } + }, + + equal: function(iconA, iconB) { + if (!this.isLoaded()) { + return false; + } + + if (this.is(iconA)) { + return angular.equals(iconA, iconB); + } else { + return false; + } + }, + }, + LabelPlugin: { + isLoaded: function() { + return angular.isDefined(L.Label); + }, + + is: function(layer) { + if (this.isLoaded()) { + return layer instanceof L.MarkerClusterGroup; + } else { + return false; + } + }, + }, + MarkerClusterPlugin: { + isLoaded: function() { + return angular.isDefined(L.MarkerClusterGroup); + }, + + is: function(layer) { + if (this.isLoaded()) { + return layer instanceof L.MarkerClusterGroup; + } else { + return false; + } + }, + }, + GoogleLayerPlugin: { + isLoaded: function() { + return angular.isDefined(L.Google); + }, + + is: function(layer) { + if (this.isLoaded()) { + return layer instanceof L.Google; + } else { + return false; + } + }, + }, + LeafletProviderPlugin: { + isLoaded: function() { + return angular.isDefined(L.TileLayer.Provider); + }, + + is: function(layer) { + if (this.isLoaded()) { + return layer instanceof L.TileLayer.Provider; + } else { + return false; + } + }, + }, + ChinaLayerPlugin: { + isLoaded: function() { + return angular.isDefined(L.tileLayer.chinaProvider); + }, + }, + HeatLayerPlugin: { + isLoaded: function() { + return angular.isDefined(L.heatLayer); + }, + }, + WebGLHeatMapLayerPlugin: { + isLoaded: function() { + return angular.isDefined(L.TileLayer.WebGLHeatMap); + }, + }, + BingLayerPlugin: { + isLoaded: function() { + return angular.isDefined(L.BingLayer); + }, + + is: function(layer) { + if (this.isLoaded()) { + return layer instanceof L.BingLayer; + } else { + return false; + } + }, + }, + WFSLayerPlugin: { + isLoaded: function() { + return L.GeoJSON.WFS !== undefined; + }, + + is: function(layer) { + if (this.isLoaded()) { + return layer instanceof L.GeoJSON.WFS; + } else { + return false; + } + }, + }, + AGSBaseLayerPlugin: { + isLoaded: function() { + return L.esri !== undefined && L.esri.basemapLayer !== undefined; + }, + + is: function(layer) { + if (this.isLoaded()) { + return layer instanceof L.esri.basemapLayer; + } else { + return false; + } + }, + }, + AGSLayerPlugin: { + isLoaded: function() { + return lvector !== undefined && lvector.AGS !== undefined; + }, + + is: function(layer) { + if (this.isLoaded()) { + return layer instanceof lvector.AGS; + } else { + return false; + } + }, + }, + AGSFeatureLayerPlugin: { + isLoaded: function() { + return L.esri !== undefined && L.esri.featureLayer !== undefined; + }, + + is: function(layer) { + if (this.isLoaded()) { + return layer instanceof L.esri.featureLayer; + } else { + return false; + } + }, + }, + AGSTiledMapLayerPlugin: { + isLoaded: function() { + return L.esri !== undefined && L.esri.tiledMapLayer !== undefined; + }, + + is: function(layer) { + if (this.isLoaded()) { + return layer instanceof L.esri.tiledMapLayer; + } else { + return false; + } + }, + }, + AGSDynamicMapLayerPlugin: { + isLoaded: function() { + return L.esri !== undefined && L.esri.dynamicMapLayer !== undefined; + }, + + is: function(layer) { + if (this.isLoaded()) { + return layer instanceof L.esri.dynamicMapLayer; + } else { + return false; + } + }, + }, + AGSImageMapLayerPlugin: { + isLoaded: function() { + return L.esri !== undefined && L.esri.imageMapLayer !== undefined; + }, + + is: function(layer) { + if (this.isLoaded()) { + return layer instanceof L.esri.imageMapLayer; + } else { + return false; + } + }, + }, + AGSClusteredLayerPlugin: { + isLoaded: function() { + return L.esri !== undefined && L.esri.clusteredFeatureLayer !== undefined; + }, + + is: function(layer) { + if (this.isLoaded()) { + return layer instanceof L.esri.clusteredFeatureLayer; + } else { + return false; + } + }, + }, + AGSHeatmapLayerPlugin: { + isLoaded: function() { + return L.esri !== undefined && L.esri.heatmapFeatureLayer !== undefined; + }, + + is: function(layer) { + if (this.isLoaded()) { + return layer instanceof L.esri.heatmapFeatureLayer; + } else { + return false; + } + }, + }, + YandexLayerPlugin: { + isLoaded: function() { + return angular.isDefined(L.Yandex); + }, + + is: function(layer) { + if (this.isLoaded()) { + return layer instanceof L.Yandex; + } else { + return false; + } + }, + }, + GeoJSONPlugin: { + isLoaded: function() { + return angular.isDefined(L.TileLayer.GeoJSON); + }, + + is: function(layer) { + if (this.isLoaded()) { + return layer instanceof L.TileLayer.GeoJSON; + } else { + return false; + } + }, + }, + UTFGridPlugin: { + isLoaded: function() { + return angular.isDefined(L.UtfGrid); + }, + + is: function(layer) { + if (this.isLoaded()) { + return layer instanceof L.UtfGrid; + } else { + $log.error('[AngularJS - Leaflet] No UtfGrid plugin found.'); + return false; + } + }, + }, + CartoDB: { + isLoaded: function() { + return cartodb; + }, + + is: function(/*layer*/) { + return true; + /* + if (this.isLoaded()) { + return layer instanceof L.TileLayer.GeoJSON; + } else { + return false; + }*/ + }, + }, + Leaflet: { + DivIcon: { + is: function(icon) { + return icon instanceof L.DivIcon; + }, + + equal: function(iconA, iconB) { + if (this.is(iconA)) { + return angular.equals(iconA, iconB); + } else { + return false; + } + }, + }, + Icon: { + is: function(icon) { + return icon instanceof L.Icon; + }, + + equal: function(iconA, iconB) { + if (this.is(iconA)) { + return angular.equals(iconA, iconB); + } else { + return false; + } + }, + }, + }, + /* + watchOptions - object to set deep nested watches and turn off watches all together + (rely on control / functional updates) + watchOptions - Object + doWatch:boolean + isDeep:boolean (sets $watch(function,isDeep)) + individual + doWatch:boolean + isDeep:boolean + */ + + //legacy defaults + watchOptions: { + doWatch:true, + isDeep: true, + individual:{ + doWatch:true, + isDeep: true, + }, + }, + }; +}]); + +angular.module('leaflet-directive').service('leafletIterators', ["$log", "leafletHelpers", function($log, leafletHelpers) { + + var lHlp = leafletHelpers; + var errorHeader = leafletHelpers.errorHeader + 'leafletIterators: '; + + //BEGIN COPY from underscore + var _keys = Object.keys; + var _isFunction = lHlp.isFunction; + var _isObject = lHlp.isObject; + + // Helper for collection methods to determine whether a collection + // should be iterated as an array or as an object + // Related: http://people.mozilla.org/~jorendorff/es6-draft.html#sec-tolength + var MAX_ARRAY_INDEX = Math.pow(2, 53) - 1; + + var _isArrayLike = function(collection) { + var length = collection !== null && collection.length; + return lHlp.isNumber(length) && length >= 0 && length <= MAX_ARRAY_INDEX; + }; + + // Keep the identity function around for default iteratees. + var _identity = function(value) { + return value; + }; + + var _property = function(key) { + return function(obj) { + return obj === null ? void 0 : obj[key]; + }; + }; + + // Internal function that returns an efficient (for current engines) version + // of the passed-in callback, to be repeatedly applied in other Underscore + // functions. + var optimizeCb = function(func, context, argCount) { + if (context === void 0) return func; + switch (argCount === null ? 3 : argCount) { + case 1: return function(value) { + return func.call(context, value); + }; + + case 2: return function(value, other) { + return func.call(context, value, other); + }; + + case 3: return function(value, index, collection) { + return func.call(context, value, index, collection); + }; + + case 4: return function(accumulator, value, index, collection) { + return func.call(context, accumulator, value, index, collection); }; } - ]); - angular.module('leaflet-directive').directive('center', [ - '$log', - '$q', - '$location', - 'leafletMapDefaults', - 'leafletHelpers', - 'leafletBoundsHelpers', - 'leafletEvents', - function ($log, $q, $location, leafletMapDefaults, leafletHelpers, leafletBoundsHelpers, leafletEvents) { - var isDefined = leafletHelpers.isDefined, isNumber = leafletHelpers.isNumber, isSameCenterOnMap = leafletHelpers.isSameCenterOnMap, safeApply = leafletHelpers.safeApply, isValidCenter = leafletHelpers.isValidCenter, isEmpty = leafletHelpers.isEmpty, isUndefinedOrEmpty = leafletHelpers.isUndefinedOrEmpty; - var shouldInitializeMapWithBounds = function (bounds, center) { - return isDefined(bounds) && !isEmpty(bounds) && isUndefinedOrEmpty(center); + return function() { + return func.apply(context, arguments); + }; + }; + + // An internal function for creating assigner functions. + var createAssigner = function(keysFunc, undefinedOnly) { + return function(obj) { + var length = arguments.length; + if (length < 2 || obj === null) return obj; + for (var index = 1; index < length; index++) { + var source = arguments[index]; + var keys = keysFunc(source); + var l = keys.length; + + for (var i = 0; i < l; i++) { + var key = keys[i]; + if (!undefinedOnly || obj[key] === void 0) obj[key] = source[key]; + } + } + + return obj; + }; + }; + + // Assigns a given object with all the own properties in the passed-in object(s) + // (https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object/assign) + var _extendOwn; + var _assign = null; + _extendOwn = _assign = createAssigner(_keys); + + // Returns whether an object has a given set of `key:value` pairs. + var _isMatch = function(object, attrs) { + var keys = _keys(attrs); + var length = keys.length; + if (object === null) return !length; + var obj = Object(object); + for (var i = 0; i < length; i++) { + var key = keys[i]; + if (attrs[key] !== obj[key] || !(key in obj)) return false; + } + + return true; + }; + + // Returns a predicate for checking whether an object has a given set of + // `key:value` pairs. + var _matcher; + var _matches = null; + _matcher = _matches = function(attrs) { + attrs = _extendOwn({}, attrs); + return function(obj) { + return _isMatch(obj, attrs); + }; + }; + + // A mostly-internal function to generate callbacks that can be applied + // to each element in a collection, returning the desired result — either + // identity, an arbitrary callback, a property matcher, or a property accessor. + var cb = function(value, context, argCount) { + if (value === null) return _identity; + if (_isFunction(value)) return optimizeCb(value, context, argCount); + if (_isObject(value)) return _matcher(value); + return _property(value); + }; + + var _every; + var _all = null; + _every = _all = function(obj, predicate, context) { + predicate = cb(predicate, context); + var keys = !_isArrayLike(obj) && _keys(obj); + var length = (keys || obj).length; + for (var index = 0; index < length; index++) { + var currentKey = keys ? keys[index] : index; + if (!predicate(obj[currentKey], currentKey, obj)) return false; + } + + return true; + }; + + //END COPY fron underscore + + var _hasErrors = function(collection, cb, ignoreCollection, cbName) { + if (!ignoreCollection) { + if (!lHlp.isDefined(collection) || !lHlp.isDefined(cb)) { + return true; + } + } + + if (!lHlp.isFunction(cb)) { + cbName = lHlp.defaultTo(cb, 'cb'); + $log.error(errorHeader + cbName + ' is not a function'); + return true; + } + + return false; + }; + + var _iterate = function(collection, externalCb, internalCb) { + if (_hasErrors(undefined, internalCb, true, 'internalCb')) { + return; + } + + if (!_hasErrors(collection, externalCb)) { + for (var key in collection) { + if (collection.hasOwnProperty(key)) { + internalCb(collection[key], key); + } + } + } + }; + + //see http://jsperf.com/iterators/3 + //utilizing for in is way faster + var _each = function(collection, cb) { + _iterate(collection, cb, function(val, key) { + cb(val, key); + }); + }; + + return { + each:_each, + forEach: _each, + every: _every, + all: _all, + }; +}]); + +angular.module('leaflet-directive') +.factory('leafletLayerHelpers', ["$rootScope", "$log", "$q", "leafletHelpers", "leafletIterators", function($rootScope, $log, $q, leafletHelpers, leafletIterators) { + var Helpers = leafletHelpers; + var isString = leafletHelpers.isString; + var isObject = leafletHelpers.isObject; + var isArray = leafletHelpers.isArray; + var isDefined = leafletHelpers.isDefined; + var errorHeader = leafletHelpers.errorHeader; + var $it = leafletIterators; + + var utfGridCreateLayer = function(params) { + if (!Helpers.UTFGridPlugin.isLoaded()) { + $log.error('[AngularJS - Leaflet] The UTFGrid plugin is not loaded.'); + return; + } + + var utfgrid = new L.UtfGrid(params.url, params.pluginOptions); + + utfgrid.on('mouseover', function(e) { + $rootScope.$broadcast('leafletDirectiveMap.utfgridMouseover', e); + }); + + utfgrid.on('mouseout', function(e) { + $rootScope.$broadcast('leafletDirectiveMap.utfgridMouseout', e); + }); + + utfgrid.on('click', function(e) { + $rootScope.$broadcast('leafletDirectiveMap.utfgridClick', e); + }); + + utfgrid.on('mousemove', function(e) { + $rootScope.$broadcast('leafletDirectiveMap.utfgridMousemove', e); + }); + + return utfgrid; + }; + + var layerTypes = { + xyz: { + mustHaveUrl: true, + createLayer: function(params) { + return L.tileLayer(params.url, params.options); + }, + }, + mapbox: { + mustHaveKey: true, + createLayer: function(params) { + var version = 3; + if (isDefined(params.options.version) && params.options.version === 4) { + version = params.options.version; + } + + var url = version === 3 ? + '//{s}.tiles.mapbox.com/v3/' + params.key + '/{z}/{x}/{y}.png' : + '//api.tiles.mapbox.com/v4/' + params.key + '/{z}/{x}/{y}.png?access_token=' + params.apiKey; + return L.tileLayer(url, params.options); + }, + }, + geoJSON: { + mustHaveUrl: true, + createLayer: function(params) { + if (!Helpers.GeoJSONPlugin.isLoaded()) { + return; + } + + return new L.TileLayer.GeoJSON(params.url, params.pluginOptions, params.options); + }, + }, + geoJSONShape: { + mustHaveUrl: false, + createLayer: function(params) { + return new L.GeoJSON(params.data, + params.options); + }, + }, + geoJSONAwesomeMarker: { + mustHaveUrl: false, + createLayer: function(params) { + return new L.geoJson(params.data, { + pointToLayer: function(feature, latlng) { + return L.marker(latlng, {icon: L.AwesomeMarkers.icon(params.icon)}); + }, + }); + }, + }, + geoJSONVectorMarker: { + mustHaveUrl: false, + createLayer: function(params) { + return new L.geoJson(params.data, { + pointToLayer: function(feature, latlng) { + return L.marker(latlng, {icon: L.VectorMarkers.icon(params.icon)}); + }, + }); + }, + }, + utfGrid: { + mustHaveUrl: true, + createLayer: utfGridCreateLayer, + }, + cartodbTiles: { + mustHaveKey: true, + createLayer: function(params) { + var url = '//' + params.user + '.cartodb.com/api/v1/map/' + params.key + '/{z}/{x}/{y}.png'; + return L.tileLayer(url, params.options); + }, + }, + cartodbUTFGrid: { + mustHaveKey: true, + mustHaveLayer: true, + createLayer: function(params) { + params.url = '//' + params.user + '.cartodb.com/api/v1/map/' + params.key + '/' + params.layer + '/{z}/{x}/{y}.grid.json'; + return utfGridCreateLayer(params); + }, + }, + cartodbInteractive: { + mustHaveKey: true, + mustHaveLayer: true, + createLayer: function(params) { + var tilesURL = '//' + params.user + '.cartodb.com/api/v1/map/' + params.key + '/{z}/{x}/{y}.png'; + var tileLayer = L.tileLayer(tilesURL, params.options); + params.url = '//' + params.user + '.cartodb.com/api/v1/map/' + params.key + '/' + params.layer + '/{z}/{x}/{y}.grid.json'; + var utfLayer = utfGridCreateLayer(params); + return L.layerGroup([tileLayer, utfLayer]); + }, + }, + wms: { + mustHaveUrl: true, + createLayer: function(params) { + return L.tileLayer.wms(params.url, params.options); + }, + }, + wmts: { + mustHaveUrl: true, + createLayer: function(params) { + return L.tileLayer.wmts(params.url, params.options); + }, + }, + wfs: { + mustHaveUrl: true, + mustHaveLayer: true, + createLayer: function(params) { + if (!Helpers.WFSLayerPlugin.isLoaded()) { + return; + } + + var options = angular.copy(params.options); + if (options.crs && typeof options.crs === 'string') { + /*jshint -W061 */ + options.crs = eval(options.crs); + } + + return new L.GeoJSON.WFS(params.url, params.layer, options); + }, + }, + group: { + mustHaveUrl: false, + createLayer: function(params) { + var lyrs = []; + $it.each(params.options.layers, function(l) { + lyrs.push(createLayer(l)); + }); + + params.options.loadedDefer = function() { + var defers = []; + if (isDefined(params.options.layers)) { + for (var i = 0; i < params.options.layers.length; i++) { + var d = params.options.layers[i].layerOptions.loadedDefer; + if (isDefined(d)) { + defers.push(d); + } + } + } + + return defers; + }; + + return L.layerGroup(lyrs); + }, + }, + featureGroup: { + mustHaveUrl: false, + createLayer: function() { + return L.featureGroup(); + }, + }, + google: { + mustHaveUrl: false, + createLayer: function(params) { + var type = params.type || 'SATELLITE'; + if (!Helpers.GoogleLayerPlugin.isLoaded()) { + return; + } + + return new L.Google(type, params.options); + }, + }, + here: { + mustHaveUrl: false, + createLayer: function(params) { + var provider = params.provider || 'HERE.terrainDay'; + if (!Helpers.LeafletProviderPlugin.isLoaded()) { + return; + } + + return new L.TileLayer.Provider(provider, params.options); + }, + }, + china:{ + mustHaveUrl:false, + createLayer:function(params) { + var type = params.type || ''; + if (!Helpers.ChinaLayerPlugin.isLoaded()) { + return; + } + + return L.tileLayer.chinaProvider(type, params.options); + }, + }, + agsBase: { + mustHaveLayer: true, + createLayer: function(params) { + if (!Helpers.AGSBaseLayerPlugin.isLoaded()) { + return; + } + + return L.esri.basemapLayer(params.layer, params.options); + }, + }, + ags: { + mustHaveUrl: true, + createLayer: function(params) { + if (!Helpers.AGSLayerPlugin.isLoaded()) { + return; + } + + var options = angular.copy(params.options); + angular.extend(options, { + url: params.url, + }); + var layer = new lvector.AGS(options); + layer.onAdd = function(map) { + this.setMap(map); + }; + + layer.onRemove = function() { + this.setMap(null); + }; + + return layer; + }, + }, + agsFeature: { + mustHaveUrl: true, + createLayer: function(params) { + if (!Helpers.AGSFeatureLayerPlugin.isLoaded()) { + $log.warn(errorHeader + ' The esri plugin is not loaded.'); + return; + } + + params.options.url = params.url; + + var layer = L.esri.featureLayer(params.options); + var load = function() { + if (isDefined(params.options.loadedDefer)) { + params.options.loadedDefer.resolve(); + } + }; + + layer.on('loading', function() { + params.options.loadedDefer = $q.defer(); + layer.off('load', load); + layer.on('load', load); + }); + + return layer; + }, + }, + agsTiled: { + mustHaveUrl: true, + createLayer: function(params) { + if (!Helpers.AGSTiledMapLayerPlugin.isLoaded()) { + $log.warn(errorHeader + ' The esri plugin is not loaded.'); + return; + } + + params.options.url = params.url; + + return L.esri.tiledMapLayer(params.options); + }, + }, + agsDynamic: { + mustHaveUrl: true, + createLayer: function(params) { + if (!Helpers.AGSDynamicMapLayerPlugin.isLoaded()) { + $log.warn(errorHeader + ' The esri plugin is not loaded.'); + return; + } + + params.options.url = params.url; + + return L.esri.dynamicMapLayer(params.options); + }, + }, + agsImage: { + mustHaveUrl: true, + createLayer: function(params) { + if (!Helpers.AGSImageMapLayerPlugin.isLoaded()) { + $log.warn(errorHeader + ' The esri plugin is not loaded.'); + return; + } + + params.options.url = params.url; + + return L.esri.imageMapLayer(params.options); + }, + }, + agsClustered: { + mustHaveUrl: true, + createLayer: function(params) { + if (!Helpers.AGSClusteredLayerPlugin.isLoaded()) { + $log.warn(errorHeader + ' The esri clustered layer plugin is not loaded.'); + return; + } + + if (!Helpers.MarkerClusterPlugin.isLoaded()) { + $log.warn(errorHeader + ' The markercluster plugin is not loaded.'); + return; + } + + return L.esri.clusteredFeatureLayer(params.url, params.options); + }, + }, + agsHeatmap: { + mustHaveUrl: true, + createLayer: function(params) { + if (!Helpers.AGSHeatmapLayerPlugin.isLoaded()) { + $log.warn(errorHeader + ' The esri heatmap layer plugin is not loaded.'); + return; + } + + if (!Helpers.HeatLayerPlugin.isLoaded()) { + $log.warn(errorHeader + ' The heatlayer plugin is not loaded.'); + return; + } + + return L.esri.heatmapFeatureLayer(params.url, params.options); + }, + }, + markercluster: { + mustHaveUrl: false, + createLayer: function(params) { + if (!Helpers.MarkerClusterPlugin.isLoaded()) { + $log.warn(errorHeader + ' The markercluster plugin is not loaded.'); + return; + } + + return new L.MarkerClusterGroup(params.options); + }, + }, + bing: { + mustHaveUrl: false, + createLayer: function(params) { + if (!Helpers.BingLayerPlugin.isLoaded()) { + return; + } + + return new L.BingLayer(params.key, params.options); + }, + }, + webGLHeatmap: { + mustHaveUrl: false, + mustHaveData: true, + createLayer: function(params) { + if (!Helpers.WebGLHeatMapLayerPlugin.isLoaded()) { + return; + } + + var layer = new L.TileLayer.WebGLHeatMap(params.options); + if (isDefined(params.data)) { + layer.setData(params.data); + } + + return layer; + }, + }, + heat: { + mustHaveUrl: false, + mustHaveData: true, + createLayer: function(params) { + if (!Helpers.HeatLayerPlugin.isLoaded()) { + return; + } + + var layer = new L.heatLayer(); + + if (isArray(params.data)) { + layer.setLatLngs(params.data); + } + + if (isObject(params.options)) { + layer.setOptions(params.options); + } + + return layer; + }, + }, + yandex: { + mustHaveUrl: false, + createLayer: function(params) { + var type = params.type || 'map'; + if (!Helpers.YandexLayerPlugin.isLoaded()) { + return; + } + + return new L.Yandex(type, params.options); + }, + }, + imageOverlay: { + mustHaveUrl: true, + mustHaveBounds: true, + createLayer: function(params) { + return L.imageOverlay(params.url, params.bounds, params.options); + }, + }, + iip: { + mustHaveUrl: true, + createLayer: function(params) { + return L.tileLayer.iip(params.url, params.options); + }, + }, + + // This "custom" type is used to accept every layer that user want to define himself. + // We can wrap these custom layers like heatmap or yandex, but it means a lot of work/code to wrap the world, + // so we let user to define their own layer outside the directive, + // and pass it on "createLayer" result for next processes + custom: { + createLayer: function(params) { + if (params.layer instanceof L.Class) { + return angular.copy(params.layer); + } else { + $log.error('[AngularJS - Leaflet] A custom layer must be a leaflet Class'); + } + }, + }, + cartodb: { + mustHaveUrl: true, + createLayer: function(params) { + return cartodb.createLayer(params.map, params.url); + }, + }, + }; + + function isValidLayerType(layerDefinition) { + // Check if the baselayer has a valid type + if (!isString(layerDefinition.type)) { + $log.error('[AngularJS - Leaflet] A layer must have a valid type defined.'); + return false; + } + + if (Object.keys(layerTypes).indexOf(layerDefinition.type) === -1) { + $log.error('[AngularJS - Leaflet] A layer must have a valid type: ' + Object.keys(layerTypes)); + return false; + } + + // Check if the layer must have an URL + if (layerTypes[layerDefinition.type].mustHaveUrl && !isString(layerDefinition.url)) { + $log.error('[AngularJS - Leaflet] A base layer must have an url'); + return false; + } + + if (layerTypes[layerDefinition.type].mustHaveData && !isDefined(layerDefinition.data)) { + $log.error('[AngularJS - Leaflet] The base layer must have a "data" array attribute'); + return false; + } + + if (layerTypes[layerDefinition.type].mustHaveLayer && !isDefined(layerDefinition.layer)) { + $log.error('[AngularJS - Leaflet] The type of layer ' + layerDefinition.type + ' must have an layer defined'); + return false; + } + + if (layerTypes[layerDefinition.type].mustHaveBounds && !isDefined(layerDefinition.bounds)) { + $log.error('[AngularJS - Leaflet] The type of layer ' + layerDefinition.type + ' must have bounds defined'); + return false; + } + + if (layerTypes[layerDefinition.type].mustHaveKey && !isDefined(layerDefinition.key)) { + $log.error('[AngularJS - Leaflet] The type of layer ' + layerDefinition.type + ' must have key defined'); + return false; + } + + return true; + } + + function createLayer(layerDefinition) { + if (!isValidLayerType(layerDefinition)) { + return; + } + + if (!isString(layerDefinition.name)) { + $log.error('[AngularJS - Leaflet] A base layer must have a name'); + return; + } + + if (!isObject(layerDefinition.layerParams)) { + layerDefinition.layerParams = {}; + } + + if (!isObject(layerDefinition.layerOptions)) { + layerDefinition.layerOptions = {}; + } + + // Mix the layer specific parameters with the general Leaflet options. Although this is an overhead + // the definition of a base layers is more 'clean' if the two types of parameters are differentiated + for (var attrname in layerDefinition.layerParams) { + layerDefinition.layerOptions[attrname] = layerDefinition.layerParams[attrname]; + } + + var params = { + url: layerDefinition.url, + data: layerDefinition.data, + options: layerDefinition.layerOptions, + layer: layerDefinition.layer, + icon: layerDefinition.icon, + type: layerDefinition.layerType, + bounds: layerDefinition.bounds, + key: layerDefinition.key, + apiKey: layerDefinition.apiKey, + pluginOptions: layerDefinition.pluginOptions, + user: layerDefinition.user, + }; + + //TODO Add $watch to the layer properties + return layerTypes[layerDefinition.type].createLayer(params); + } + + function safeAddLayer(map, layer) { + if (layer && typeof layer.addTo === 'function') { + layer.addTo(map); + } else { + map.addLayer(layer); + } + } + + function safeRemoveLayer(map, layer, layerOptions) { + if (isDefined(layerOptions) && isDefined(layerOptions.loadedDefer)) { + if (angular.isFunction(layerOptions.loadedDefer)) { + var defers = layerOptions.loadedDefer(); + $log.debug('Loaded Deferred', defers); + var count = defers.length; + if (count > 0) { + var resolve = function() { + count--; + if (count === 0) { + map.removeLayer(layer); + } + }; + + for (var i = 0; i < defers.length; i++) { + defers[i].promise.then(resolve); + } + } else { + map.removeLayer(layer); + } + } else { + layerOptions.loadedDefer.promise.then(function() { + map.removeLayer(layer); + }); + } + } else { + map.removeLayer(layer); + } + } + + return { + createLayer: createLayer, + safeAddLayer: safeAddLayer, + safeRemoveLayer: safeRemoveLayer, + }; +}]); + +angular.module('leaflet-directive').factory('leafletLegendHelpers', function() { + var _updateLegend = function(div, legendData, type, url) { + div.innerHTML = ''; + if (legendData.error) { + div.innerHTML += '
        ' + legendData.error.message + '
        '; + } else { + if (type === 'arcgis') { + for (var i = 0; i < legendData.layers.length; i++) { + var layer = legendData.layers[i]; + div.innerHTML += '
        ' + layer.layerName + '
        '; + for (var j = 0; j < layer.legend.length; j++) { + var leg = layer.legend[j]; + div.innerHTML += + '
        ' + + '
        ' + leg.label + '
        '; + } + } + } else if (type === 'image') { + div.innerHTML = ''; + } + } + }; + + var _getOnAddLegend = function(legendData, legendClass, type, url) { + return function(/*map*/) { + var div = L.DomUtil.create('div', legendClass); + + if (!L.Browser.touch) { + L.DomEvent.disableClickPropagation(div); + L.DomEvent.on(div, 'mousewheel', L.DomEvent.stopPropagation); + } else { + L.DomEvent.on(div, 'click', L.DomEvent.stopPropagation); + } + + _updateLegend(div, legendData, type, url); + return div; + }; + }; + + var _getOnAddArrayLegend = function(legend, legendClass) { + return function(/*map*/) { + var div = L.DomUtil.create('div', legendClass); + for (var i = 0; i < legend.colors.length; i++) { + div.innerHTML += + '
        ' + + '
        ' + legend.labels[i] + '
        '; + } + + if (!L.Browser.touch) { + L.DomEvent.disableClickPropagation(div); + L.DomEvent.on(div, 'mousewheel', L.DomEvent.stopPropagation); + } else { + L.DomEvent.on(div, 'click', L.DomEvent.stopPropagation); + } + + return div; + }; + }; + + return { + getOnAddLegend: _getOnAddLegend, + getOnAddArrayLegend: _getOnAddArrayLegend, + updateLegend: _updateLegend, + }; +}); + +angular.module('leaflet-directive').factory('leafletMapDefaults', ["$q", "leafletHelpers", function($q, leafletHelpers) { + function _getDefaults() { + return { + keyboard: true, + dragging: true, + worldCopyJump: false, + doubleClickZoom: true, + scrollWheelZoom: true, + tap: true, + touchZoom: true, + zoomControl: true, + zoomsliderControl: false, + zoomControlPosition: 'topleft', + attributionControl: true, + controls: { + layers: { + visible: true, + position: 'topright', + collapsed: true, + }, + }, + nominatim: { + server: ' http://nominatim.openstreetmap.org/search', + }, + crs: L.CRS.EPSG3857, + tileLayer: '//{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', + tileLayerOptions: { + attribution: '© OpenStreetMap contributors', + }, + path: { + weight: 10, + opacity: 1, + color: '#0000ff', + }, + center: { + lat: 0, + lng: 0, + zoom: 1, + }, + }; + } + + var isDefined = leafletHelpers.isDefined; + var isObject = leafletHelpers.isObject; + var obtainEffectiveMapId = leafletHelpers.obtainEffectiveMapId; + var defaults = {}; + + // Get the _defaults dictionary, and override the properties defined by the user + return { + reset: function() { + defaults = {}; + }, + + getDefaults: function(scopeId) { + var mapId = obtainEffectiveMapId(defaults, scopeId); + return defaults[mapId]; + }, + + getMapCreationDefaults: function(scopeId) { + var mapId = obtainEffectiveMapId(defaults, scopeId); + var d = defaults[mapId]; + + var mapDefaults = { + maxZoom: d.maxZoom, + keyboard: d.keyboard, + dragging: d.dragging, + zoomControl: d.zoomControl, + doubleClickZoom: d.doubleClickZoom, + scrollWheelZoom: d.scrollWheelZoom, + tap: d.tap, + touchZoom: d.touchZoom, + attributionControl: d.attributionControl, + worldCopyJump: d.worldCopyJump, + crs: d.crs, }; + + if (isDefined(d.minZoom)) { + mapDefaults.minZoom = d.minZoom; + } + + if (isDefined(d.zoomAnimation)) { + mapDefaults.zoomAnimation = d.zoomAnimation; + } + + if (isDefined(d.fadeAnimation)) { + mapDefaults.fadeAnimation = d.fadeAnimation; + } + + if (isDefined(d.markerZoomAnimation)) { + mapDefaults.markerZoomAnimation = d.markerZoomAnimation; + } + + if (d.map) { + for (var option in d.map) { + mapDefaults[option] = d.map[option]; + } + } + + return mapDefaults; + }, + + setDefaults: function(userDefaults, scopeId) { + var newDefaults = _getDefaults(); + + if (isDefined(userDefaults)) { + newDefaults.doubleClickZoom = isDefined(userDefaults.doubleClickZoom) ? userDefaults.doubleClickZoom : newDefaults.doubleClickZoom; + newDefaults.scrollWheelZoom = isDefined(userDefaults.scrollWheelZoom) ? userDefaults.scrollWheelZoom : newDefaults.doubleClickZoom; + newDefaults.tap = isDefined(userDefaults.tap) ? userDefaults.tap : newDefaults.tap; + newDefaults.touchZoom = isDefined(userDefaults.touchZoom) ? userDefaults.touchZoom : newDefaults.doubleClickZoom; + newDefaults.zoomControl = isDefined(userDefaults.zoomControl) ? userDefaults.zoomControl : newDefaults.zoomControl; + newDefaults.zoomsliderControl = isDefined(userDefaults.zoomsliderControl) ? userDefaults.zoomsliderControl : newDefaults.zoomsliderControl; + newDefaults.attributionControl = isDefined(userDefaults.attributionControl) ? userDefaults.attributionControl : newDefaults.attributionControl; + newDefaults.tileLayer = isDefined(userDefaults.tileLayer) ? userDefaults.tileLayer : newDefaults.tileLayer; + newDefaults.zoomControlPosition = isDefined(userDefaults.zoomControlPosition) ? userDefaults.zoomControlPosition : newDefaults.zoomControlPosition; + newDefaults.keyboard = isDefined(userDefaults.keyboard) ? userDefaults.keyboard : newDefaults.keyboard; + newDefaults.dragging = isDefined(userDefaults.dragging) ? userDefaults.dragging : newDefaults.dragging; + + if (isDefined(userDefaults.controls)) { + angular.extend(newDefaults.controls, userDefaults.controls); + } + + if (isObject(userDefaults.crs)) { + newDefaults.crs = userDefaults.crs; + } else if (isDefined(L.CRS[userDefaults.crs])) { + newDefaults.crs = L.CRS[userDefaults.crs]; + } + + if (isDefined(userDefaults.center)) { + angular.copy(userDefaults.center, newDefaults.center); + } + + if (isDefined(userDefaults.tileLayerOptions)) { + angular.copy(userDefaults.tileLayerOptions, newDefaults.tileLayerOptions); + } + + if (isDefined(userDefaults.maxZoom)) { + newDefaults.maxZoom = userDefaults.maxZoom; + } + + if (isDefined(userDefaults.minZoom)) { + newDefaults.minZoom = userDefaults.minZoom; + } + + if (isDefined(userDefaults.zoomAnimation)) { + newDefaults.zoomAnimation = userDefaults.zoomAnimation; + } + + if (isDefined(userDefaults.fadeAnimation)) { + newDefaults.fadeAnimation = userDefaults.fadeAnimation; + } + + if (isDefined(userDefaults.markerZoomAnimation)) { + newDefaults.markerZoomAnimation = userDefaults.markerZoomAnimation; + } + + if (isDefined(userDefaults.worldCopyJump)) { + newDefaults.worldCopyJump = userDefaults.worldCopyJump; + } + + if (isDefined(userDefaults.map)) { + newDefaults.map = userDefaults.map; + } + + if (isDefined(userDefaults.path)) { + newDefaults.path = userDefaults.path; + } + } + + var mapId = obtainEffectiveMapId(defaults, scopeId); + defaults[mapId] = newDefaults; + return newDefaults; + }, + }; +}]); + +angular.module('leaflet-directive').service('leafletMarkersHelpers', ["$rootScope", "$timeout", "leafletHelpers", "$log", "$compile", "leafletGeoJsonHelpers", function($rootScope, $timeout, leafletHelpers, $log, $compile, leafletGeoJsonHelpers) { + var isDefined = leafletHelpers.isDefined; + var defaultTo = leafletHelpers.defaultTo; + var MarkerClusterPlugin = leafletHelpers.MarkerClusterPlugin; + var AwesomeMarkersPlugin = leafletHelpers.AwesomeMarkersPlugin; + var VectorMarkersPlugin = leafletHelpers.VectorMarkersPlugin; + var MakiMarkersPlugin = leafletHelpers.MakiMarkersPlugin; + var ExtraMarkersPlugin = leafletHelpers.ExtraMarkersPlugin; + var DomMarkersPlugin = leafletHelpers.DomMarkersPlugin; + var safeApply = leafletHelpers.safeApply; + var Helpers = leafletHelpers; + var isString = leafletHelpers.isString; + var isNumber = leafletHelpers.isNumber; + var isObject = leafletHelpers.isObject; + var groups = {}; + var geoHlp = leafletGeoJsonHelpers; + var errorHeader = leafletHelpers.errorHeader; + + var _string = function(marker) { + //this exists since JSON.stringify barfs on cyclic + var retStr = ''; + ['_icon', '_latlng', '_leaflet_id', '_map', '_shadow'].forEach(function(prop) { + retStr += prop + ': ' + defaultTo(marker[prop], 'undefined') + ' \n'; + }); + + return '[leafletMarker] : \n' + retStr; + }; + + var _log = function(marker, useConsole) { + var logger = useConsole ? console : $log; + logger.debug(_string(marker)); + }; + + var createLeafletIcon = function(iconData) { + if (isDefined(iconData) && isDefined(iconData.type) && iconData.type === 'awesomeMarker') { + if (!AwesomeMarkersPlugin.isLoaded()) { + $log.error(errorHeader + ' The AwesomeMarkers Plugin is not loaded.'); + } + + return new L.AwesomeMarkers.icon(iconData); + } + + if (isDefined(iconData) && isDefined(iconData.type) && iconData.type === 'vectorMarker') { + if (!VectorMarkersPlugin.isLoaded()) { + $log.error(errorHeader + ' The VectorMarkers Plugin is not loaded.'); + } + + return new L.VectorMarkers.icon(iconData); + } + + if (isDefined(iconData) && isDefined(iconData.type) && iconData.type === 'makiMarker') { + if (!MakiMarkersPlugin.isLoaded()) { + $log.error(errorHeader + 'The MakiMarkers Plugin is not loaded.'); + } + + return new L.MakiMarkers.icon(iconData); + } + + if (isDefined(iconData) && isDefined(iconData.type) && iconData.type === 'extraMarker') { + if (!ExtraMarkersPlugin.isLoaded()) { + $log.error(errorHeader + 'The ExtraMarkers Plugin is not loaded.'); + } + + return new L.ExtraMarkers.icon(iconData); + } + + if (isDefined(iconData) && isDefined(iconData.type) && iconData.type === 'div') { + return new L.divIcon(iconData); + } + + if (isDefined(iconData) && isDefined(iconData.type) && iconData.type === 'dom') { + if (!DomMarkersPlugin.isLoaded()) { + $log.error(errorHeader + 'The DomMarkers Plugin is not loaded.'); + } + + var markerScope = angular.isFunction(iconData.getMarkerScope) ? iconData.getMarkerScope() : $rootScope; + var template = $compile(iconData.template)(markerScope); + var iconDataCopy = angular.copy(iconData); + iconDataCopy.element = template[0]; + return new L.DomMarkers.icon(iconDataCopy); + } + + // allow for any custom icon to be used... assumes the icon has already been initialized + if (isDefined(iconData) && isDefined(iconData.type) && iconData.type === 'icon') { + return iconData.icon; + } + + var base64icon = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABkAAAApCAYAAADAk4LOAAAGmklEQVRYw7VXeUyTZxjvNnfELFuyIzOabermMZEeQC/OclkO49CpOHXOLJl/CAURuYbQi3KLgEhbrhZ1aDwmaoGqKII6odATmH/scDFbdC7LvFqOCc+e95s2VG50X/LLm/f4/Z7neY/ne18aANCmAr5E/xZf1uDOkTcGcWR6hl9247tT5U7Y6SNvWsKT63P58qbfeLJG8M5qcgTknrvvrdDbsT7Ml+tv82X6vVxJE33aRmgSyYtcWVMqX97Yv2JvW39UhRE2HuyBL+t+gK1116ly06EeWFNlAmHxlQE0OMiV6mQCScusKRlhS3QLeVJdl1+23h5dY4FNB3thrbYboqptEFlphTC1hSpJnbRvxP4NWgsE5Jyz86QNNi/5qSUTGuFk1gu54tN9wuK2wc3o+Wc13RCmsoBwEqzGcZsxsvCSy/9wJKf7UWf1mEY8JWfewc67UUoDbDjQC+FqK4QqLVMGGR9d2wurKzqBk3nqIT/9zLxRRjgZ9bqQgub+DdoeCC03Q8j+0QhFhBHR/eP3U/zCln7Uu+hihJ1+bBNffLIvmkyP0gpBZWYXhKussK6mBz5HT6M1Nqpcp+mBCPXosYQfrekGvrjewd59/GvKCE7TbK/04/ZV5QZYVWmDwH1mF3xa2Q3ra3DBC5vBT1oP7PTj4C0+CcL8c7C2CtejqhuCnuIQHaKHzvcRfZpnylFfXsYJx3pNLwhKzRAwAhEqG0SpusBHfAKkxw3w4627MPhoCH798z7s0ZnBJ/MEJbZSbXPhER2ih7p2ok/zSj2cEJDd4CAe+5WYnBCgR2uruyEw6zRoW6/DWJ/OeAP8pd/BGtzOZKpG8oke0SX6GMmRk6GFlyAc59K32OTEinILRJRchah8HQwND8N435Z9Z0FY1EqtxUg+0SO6RJ/mmXz4VuS+DpxXC3gXmZwIL7dBSH4zKE50wESf8qwVgrP1EIlTO5JP9Igu0aexdh28F1lmAEGJGfh7jE6ElyM5Rw/FDcYJjWhbeiBYoYNIpc2FT/SILivp0F1ipDWk4BIEo2VuodEJUifhbiltnNBIXPUFCMpthtAyqws/BPlEF/VbaIxErdxPphsU7rcCp8DohC+GvBIPJS/tW2jtvTmmAeuNO8BNOYQeG8G/2OzCJ3q+soYB5i6NhMaKr17FSal7GIHheuV3uSCY8qYVuEm1cOzqdWr7ku/R0BDoTT+DT+ohCM6/CCvKLKO4RI+dXPeAuaMqksaKrZ7L3FE5FIFbkIceeOZ2OcHO6wIhTkNo0ffgjRGxEqogXHYUPHfWAC/lADpwGcLRY3aeK4/oRGCKYcZXPVoeX/kelVYY8dUGf8V5EBRbgJXT5QIPhP9ePJi428JKOiEYhYXFBqou2Guh+p/mEB1/RfMw6rY7cxcjTrneI1FrDyuzUSRm9miwEJx8E/gUmqlyvHGkneiwErR21F3tNOK5Tf0yXaT+O7DgCvALTUBXdM4YhC/IawPU+2PduqMvuaR6eoxSwUk75ggqsYJ7VicsnwGIkZBSXKOUww73WGXyqP+J2/b9c+gi1YAg/xpwck3gJuucNrh5JvDPvQr0WFXf0piyt8f8/WI0hV4pRxxkQZdJDfDJNOAmM0Ag8jyT6hz0WGXWuP94Yh2jcfjmXAGvHCMslRimDHYuHuDsy2QtHuIavznhbYURq5R57KpzBBRZKPJi8eQg48h4j8SDdowifdIrEVdU+gbO6QNvRRt4ZBthUaZhUnjlYObNagV3keoeru3rU7rcuceqU1mJBxy+BWZYlNEBH+0eH4vRiB+OYybU2hnblYlTvkHinM4m54YnxSyaZYSF6R3jwgP7udKLGIX6r/lbNa9N6y5MFynjWDtrHd75ZvTYAPO/6RgF0k76mQla3FGq7dO+cH8sKn0Vo7nDllwAhqwLPkxrHwWmHJOo+AKJ4rab5OgrM7rVu8eWb2Pu0Dh4eDgXoOfvp7Y7QeqknRmvcTBEyq9m/HQQSCSz6LHq3z0yzsNySRfMS253wl2KyRDbcZPcfJKjZmSEOjcxyi+Y8dUOtsIEH6R2wNykdqrkYJ0RV92H0W58pkfQk7cKevsLK10Py8SdMGfXNXATY+pPbyJR/ET6n9nIfztNtZYRV9XniQu9IA2vOVgy4ir7GCLVmmd+zjkH0eAF9Po6K61pmCXHxU5rHMYd1ftc3owjwRSVRzLjKvqZEty6cRUD7jGqiOdu5HG6MdHjNcNYGqfDm5YRzLBBCCDl/2bk8a8gdbqcfwECu62Fg/HrggAAAABJRU5ErkJggg=='; + var base64shadow = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACkAAAApCAYAAACoYAD2AAAC5ElEQVRYw+2YW4/TMBCF45S0S1luXZCABy5CgLQgwf//S4BYBLTdJLax0fFqmB07nnQfEGqkIydpVH85M+NLjPe++dcPc4Q8Qh4hj5D/AaQJx6H/4TMwB0PeBNwU7EGQAmAtsNfAzoZkgIa0ZgLMa4Aj6CxIAsjhjOCoL5z7Glg1JAOkaicgvQBXuncwJAWjksLtBTWZe04CnYRktUGdilALppZBOgHGZcBzL6OClABvMSVIzyBjazOgrvACf1ydC5mguqAVg6RhdkSWQFj2uxfaq/BrIZOLEWgZdALIDvcMcZLD8ZbLC9de4yR1sYMi4G20S4Q/PWeJYxTOZn5zJXANZHIxAd4JWhPIloTJZhzMQduM89WQ3MUVAE/RnhAXpTycqys3NZALOBbB7kFrgLesQl2h45Fcj8L1tTSohUwuxhy8H/Qg6K7gIs+3kkaigQCOcyEXCHN07wyQazhrmIulvKMQAwMcmLNqyCVyMAI+BuxSMeTk3OPikLY2J1uE+VHQk6ANrhds+tNARqBeaGc72cK550FP4WhXmFmcMGhTwAR1ifOe3EvPqIegFmF+C8gVy0OfAaWQPMR7gF1OQKqGoBjq90HPMP01BUjPOqGFksC4emE48tWQAH0YmvOgF3DST6xieJgHAWxPAHMuNhrImIdvoNOKNWIOcE+UXE0pYAnkX6uhWsgVXDxHdTfCmrEEmMB2zMFimLVOtiiajxiGWrbU52EeCdyOwPEQD8LqyPH9Ti2kgYMf4OhSKB7qYILbBv3CuVTJ11Y80oaseiMWOONc/Y7kJYe0xL2f0BaiFTxknHO5HaMGMublKwxFGzYdWsBF174H/QDknhTHmHHN39iWFnkZx8lPyM8WHfYELmlLKtgWNmFNzQcC1b47gJ4hL19i7o65dhH0Negbca8vONZoP7doIeOC9zXm8RjuL0Gf4d4OYaU5ljo3GYiqzrWQHfJxA6ALhDpVKv9qYeZA8eM3EhfPSCmpuD0AAAAASUVORK5CYII='; + + if (!isDefined(iconData) || !isDefined(iconData.iconUrl)) { + return new L.Icon.Default({ + iconUrl: base64icon, + shadowUrl: base64shadow, + iconSize: [25, 41], + iconAnchor: [12, 41], + popupAnchor: [1, -34], + shadowSize: [41, 41], + }); + } + + return new L.Icon(iconData); + }; + + var _resetMarkerGroup = function(groupName) { + if (isDefined(groups[groupName])) { + groups.splice(groupName, 1); + } + }; + + var _resetMarkerGroups = function() { + groups = {}; + }; + + var _deleteMarker = function(marker, map, layers) { + marker.closePopup(); + + // There is no easy way to know if a marker is added to a layer, so we search for it + // if there are overlays + if (isDefined(layers) && isDefined(layers.overlays)) { + for (var key in layers.overlays) { + if (layers.overlays[key] instanceof L.LayerGroup || layers.overlays[key] instanceof L.FeatureGroup) { + if (layers.overlays[key].hasLayer(marker)) { + layers.overlays[key].removeLayer(marker); + return; + } + } + } + } + + if (isDefined(groups)) { + for (var groupKey in groups) { + if (groups[groupKey].hasLayer(marker)) { + groups[groupKey].removeLayer(marker); + } + } + } + + if (map.hasLayer(marker)) { + map.removeLayer(marker); + } + }; + + var adjustPopupPan = function(marker, map) { + var containerHeight = marker._popup._container.offsetHeight; + var layerPos = new L.Point(marker._popup._containerLeft, -containerHeight - marker._popup._containerBottom); + var containerPos = map.layerPointToContainerPoint(layerPos); + if (containerPos !== null) { + marker._popup._adjustPan(); + } + }; + + var compilePopup = function(marker, markerScope) { + $compile(marker._popup._contentNode)(markerScope); + }; + + var updatePopup = function(marker, markerScope, map) { + //The innerText should be more than 1 once angular has compiled. + //We need to keep trying until angular has compiled before we _updateLayout and _updatePosition + //This should take care of any scenario , eg ngincludes, whatever. + //Is there a better way to check for this? + var innerText = marker._popup._contentNode.innerText || marker._popup._contentNode.textContent; + if (innerText.length < 1) { + $timeout(function() { + updatePopup(marker, markerScope, map); + }); + } + + //cause a reflow - this is also very important - if we don't do this then the widths are from before $compile + var reflow = marker._popup._contentNode.offsetWidth; + + marker._popup._updateLayout(); + marker._popup._updatePosition(); + + if (marker._popup.options.autoPan) { + adjustPopupPan(marker, map); + } + + //using / returning reflow so jshint doesn't moan + return reflow; + }; + + var _manageOpenPopup = function(marker, markerData, map) { + // The marker may provide a scope returning function used to compile the message + // default to $rootScope otherwise + var markerScope = angular.isFunction(markerData.getMessageScope) ? markerData.getMessageScope() : $rootScope; + var compileMessage = isDefined(markerData.compileMessage) ? markerData.compileMessage : true; + + if (compileMessage) { + if (!isDefined(marker._popup) || !isDefined(marker._popup._contentNode)) { + $log.error(errorHeader + 'Popup is invalid or does not have any content.'); + return false; + } + + compilePopup(marker, markerScope); + updatePopup(marker, markerData, map); + } + }; + + var _manageOpenLabel = function(marker, markerData) { + var markerScope = angular.isFunction(markerData.getMessageScope) ? markerData.getMessageScope() : $rootScope; + var labelScope = angular.isFunction(markerData.getLabelScope) ? markerData.getLabelScope() : markerScope; + var compileMessage = isDefined(markerData.compileMessage) ? markerData.compileMessage : true; + + if (Helpers.LabelPlugin.isLoaded() && isDefined(markerData.label)) { + if (isDefined(markerData.label.options) && markerData.label.options.noHide === true) { + marker.showLabel(); + } + + if (compileMessage && isDefined(marker.label)) { + $compile(marker.label._container)(labelScope); + } + } + }; + + var _updateMarker = function(markerData, oldMarkerData, marker, name, leafletScope, layers, map) { + if (!isDefined(oldMarkerData)) { + return; + } + + // Update the lat-lng property (always present in marker properties) + if (!geoHlp.validateCoords(markerData)) { + $log.warn('There are problems with lat-lng data, please verify your marker model'); + _deleteMarker(marker, map, layers); + return; + } + + // watch is being initialized if old and new object is the same + var isInitializing = markerData === oldMarkerData; + + // Update marker rotation + if (isDefined(markerData.iconAngle) && oldMarkerData.iconAngle !== markerData.iconAngle) { + marker.setIconAngle(markerData.iconAngle); + } + + // It is possible that the layer has been removed or the layer marker does not exist + // Update the layer group if present or move it to the map if not + if (!isString(markerData.layer)) { + // There is no layer information, we move the marker to the map if it was in a layer group + if (isString(oldMarkerData.layer)) { + // Remove from the layer group that is supposed to be + if (isDefined(layers.overlays[oldMarkerData.layer]) && layers.overlays[oldMarkerData.layer].hasLayer(marker)) { + layers.overlays[oldMarkerData.layer].removeLayer(marker); + marker.closePopup(); + } + + // Test if it is not on the map and add it + if (!map.hasLayer(marker)) { + map.addLayer(marker); + } + } + } + + if ((isNumber(markerData.opacity) || isNumber(parseFloat(markerData.opacity))) && markerData.opacity !== oldMarkerData.opacity) { + // There was a different opacity so we update it + marker.setOpacity(markerData.opacity); + } + + if (isString(markerData.layer) && oldMarkerData.layer !== markerData.layer) { + // If it was on a layer group we have to remove it + if (isString(oldMarkerData.layer) && isDefined(layers.overlays[oldMarkerData.layer]) && layers.overlays[oldMarkerData.layer].hasLayer(marker)) { + layers.overlays[oldMarkerData.layer].removeLayer(marker); + } + + marker.closePopup(); + + // Remove it from the map in case the new layer is hidden or there is an error in the new layer + if (map.hasLayer(marker)) { + map.removeLayer(marker); + } + + // The markerData.layer is defined so we add the marker to the layer if it is different from the old data + if (!isDefined(layers.overlays[markerData.layer])) { + $log.error(errorHeader + 'You must use a name of an existing layer'); + return; + } + + // Is a group layer? + var layerGroup = layers.overlays[markerData.layer]; + if (!(layerGroup instanceof L.LayerGroup || layerGroup instanceof L.FeatureGroup)) { + $log.error(errorHeader + 'A marker can only be added to a layer of type "group" or "featureGroup"'); + return; + } + + // The marker goes to a correct layer group, so first of all we add it + layerGroup.addLayer(marker); + + // The marker is automatically added to the map depending on the visibility + // of the layer, so we only have to open the popup if the marker is in the map + if (map.hasLayer(marker) && markerData.focus === true) { + marker.openPopup(); + } + } + + // Update the draggable property + if (markerData.draggable !== true && oldMarkerData.draggable === true && (isDefined(marker.dragging))) { + marker.dragging.disable(); + } + + if (markerData.draggable === true && oldMarkerData.draggable !== true) { + // The markerData.draggable property must be true so we update if there wasn't a previous value or it wasn't true + if (marker.dragging) { + marker.dragging.enable(); + } else { + if (L.Handler.MarkerDrag) { + marker.dragging = new L.Handler.MarkerDrag(marker); + marker.options.draggable = true; + marker.dragging.enable(); + } + } + } + + // Update the icon property + if (!isObject(markerData.icon)) { + // If there is no icon property or it's not an object + if (isObject(oldMarkerData.icon)) { + // If there was an icon before restore to the default + marker.setIcon(createLeafletIcon()); + marker.closePopup(); + marker.unbindPopup(); + if (isString(markerData.message)) { + marker.bindPopup(markerData.message, markerData.popupOptions); + } + } + } + + if (isObject(markerData.icon) && isObject(oldMarkerData.icon) && !angular.equals(markerData.icon, oldMarkerData.icon)) { + var dragG = false; + if (marker.dragging) { + dragG = marker.dragging.enabled(); + } + + marker.setIcon(createLeafletIcon(markerData.icon)); + if (dragG) { + marker.dragging.enable(); + } + + marker.closePopup(); + marker.unbindPopup(); + if (isString(markerData.message)) { + marker.bindPopup(markerData.message, markerData.popupOptions); + + // if marker has been already focused, reopen popup + if (map.hasLayer(marker) && markerData.focus === true) { + marker.openPopup(); + } + } + } + + // Update the Popup message property + if (!isString(markerData.message) && isString(oldMarkerData.message)) { + marker.closePopup(); + marker.unbindPopup(); + } + + // Update the label content or bind a new label if the old one has been removed. + if (Helpers.LabelPlugin.isLoaded()) { + if (isDefined(markerData.label) && isDefined(markerData.label.message)) { + if ('label' in oldMarkerData && 'message' in oldMarkerData.label && !angular.equals(markerData.label.message, oldMarkerData.label.message)) { + marker.updateLabelContent(markerData.label.message); + } else if (!angular.isFunction(marker.getLabel) || angular.isFunction(marker.getLabel) && !isDefined(marker.getLabel())) { + marker.bindLabel(markerData.label.message, markerData.label.options); + _manageOpenLabel(marker, markerData); + } else { + _manageOpenLabel(marker, markerData); + } + } else if (!('label' in markerData && !('message' in markerData.label))) { + if (angular.isFunction(marker.unbindLabel)) { + marker.unbindLabel(); + } + } + } + + // There is some text in the popup, so we must show the text or update existing + if (isString(markerData.message) && !isString(oldMarkerData.message)) { + // There was no message before so we create it + marker.bindPopup(markerData.message, markerData.popupOptions); + } + + if (isString(markerData.message) && isString(oldMarkerData.message) && markerData.message !== oldMarkerData.message) { + // There was a different previous message so we update it + marker.setPopupContent(markerData.message); + } + + // Update the focus property + var updatedFocus = false; + if (markerData.focus !== true && oldMarkerData.focus === true) { + // If there was a focus property and was true we turn it off + marker.closePopup(); + updatedFocus = true; + } + + // The markerData.focus property must be true so we update if there wasn't a previous value or it wasn't true + if (markerData.focus === true && (!isDefined(oldMarkerData.focus) || oldMarkerData.focus === false) || (isInitializing && markerData.focus === true)) { + // Reopen the popup when focus is still true + marker.openPopup(); + updatedFocus = true; + } + + // zIndexOffset adjustment + if (oldMarkerData.zIndexOffset !== markerData.zIndexOffset) { + marker.setZIndexOffset(markerData.zIndexOffset); + } + + var markerLatLng = marker.getLatLng(); + var isCluster = (isString(markerData.layer) && Helpers.MarkerClusterPlugin.is(layers.overlays[markerData.layer])); + + // If the marker is in a cluster it has to be removed and added to the layer when the location is changed + if (isCluster) { + // The focus has changed even by a user click or programatically + if (updatedFocus) { + // We only have to update the location if it was changed programatically, because it was + // changed by a user drag the marker data has already been updated by the internal event + // listened by the directive + if ((markerData.lat !== oldMarkerData.lat) || (markerData.lng !== oldMarkerData.lng)) { + layers.overlays[markerData.layer].removeLayer(marker); + marker.setLatLng([markerData.lat, markerData.lng]); + layers.overlays[markerData.layer].addLayer(marker); + } + } else { + // The marker has possibly moved. It can be moved by a user drag (marker location and data are equal but old + // data is diferent) or programatically (marker location and data are diferent) + if ((markerLatLng.lat !== markerData.lat) || (markerLatLng.lng !== markerData.lng)) { + // The marker was moved by a user drag + layers.overlays[markerData.layer].removeLayer(marker); + marker.setLatLng([markerData.lat, markerData.lng]); + layers.overlays[markerData.layer].addLayer(marker); + } else if ((markerData.lat !== oldMarkerData.lat) || (markerData.lng !== oldMarkerData.lng)) { + // The marker was moved programatically + layers.overlays[markerData.layer].removeLayer(marker); + marker.setLatLng([markerData.lat, markerData.lng]); + layers.overlays[markerData.layer].addLayer(marker); + } else if (isObject(markerData.icon) && isObject(oldMarkerData.icon) && !angular.equals(markerData.icon, oldMarkerData.icon)) { + layers.overlays[markerData.layer].removeLayer(marker); + layers.overlays[markerData.layer].addLayer(marker); + } + } + } else if (markerLatLng.lat !== markerData.lat || markerLatLng.lng !== markerData.lng) { + marker.setLatLng([markerData.lat, markerData.lng]); + } + }; + + return { + resetMarkerGroup: _resetMarkerGroup, + + resetMarkerGroups: _resetMarkerGroups, + + deleteMarker: _deleteMarker, + + manageOpenPopup: _manageOpenPopup, + + manageOpenLabel: _manageOpenLabel, + + createMarker: function(markerData) { + if (!isDefined(markerData) || !geoHlp.validateCoords(markerData)) { + $log.error(errorHeader + 'The marker definition is not valid.'); + return; + } + + var coords = geoHlp.getCoords(markerData); + + if (!isDefined(coords)) { + $log.error(errorHeader + 'Unable to get coordinates from markerData.'); + return; + } + + var markerOptions = { + icon: createLeafletIcon(markerData.icon), + title: isDefined(markerData.title) ? markerData.title : '', + draggable: isDefined(markerData.draggable) ? markerData.draggable : false, + clickable: isDefined(markerData.clickable) ? markerData.clickable : true, + riseOnHover: isDefined(markerData.riseOnHover) ? markerData.riseOnHover : false, + zIndexOffset: isDefined(markerData.zIndexOffset) ? markerData.zIndexOffset : 0, + iconAngle: isDefined(markerData.iconAngle) ? markerData.iconAngle : 0, + }; + + // Add any other options not added above to markerOptions + for (var markerDatum in markerData) { + if (markerData.hasOwnProperty(markerDatum) && !markerOptions.hasOwnProperty(markerDatum)) { + markerOptions[markerDatum] = markerData[markerDatum]; + } + } + + var marker = new L.marker(coords, markerOptions); + + if (!isString(markerData.message)) { + marker.unbindPopup(); + } + + return marker; + }, + + addMarkerToGroup: function(marker, groupName, groupOptions, map) { + if (!isString(groupName)) { + $log.error(errorHeader + 'The marker group you have specified is invalid.'); + return; + } + + if (!MarkerClusterPlugin.isLoaded()) { + $log.error(errorHeader + 'The MarkerCluster plugin is not loaded.'); + return; + } + + if (!isDefined(groups[groupName])) { + groups[groupName] = new L.MarkerClusterGroup(groupOptions); + map.addLayer(groups[groupName]); + } + + groups[groupName].addLayer(marker); + }, + + listenMarkerEvents: function(marker, markerData, leafletScope, doWatch, map) { + marker.on('popupopen', function(/* event */) { + safeApply(leafletScope, function() { + if (isDefined(marker._popup) || isDefined(marker._popup._contentNode)) { + markerData.focus = true; + _manageOpenPopup(marker, markerData, map);//needed since markerData is now a copy + } + }); + }); + + marker.on('popupclose', function(/* event */) { + safeApply(leafletScope, function() { + markerData.focus = false; + }); + }); + + marker.on('add', function(/* event */) { + safeApply(leafletScope, function() { + if ('label' in markerData) + _manageOpenLabel(marker, markerData); + }); + }); + }, + + updateMarker: _updateMarker, + + addMarkerWatcher: function(marker, name, leafletScope, layers, map, isDeepWatch) { + var markerWatchPath = Helpers.getObjectArrayPath('markers.' + name); + isDeepWatch = defaultTo(isDeepWatch, true); + + var clearWatch = leafletScope.$watch(markerWatchPath, function(markerData, oldMarkerData) { + if (!isDefined(markerData)) { + _deleteMarker(marker, map, layers); + clearWatch(); + return; + } + + _updateMarker(markerData, oldMarkerData, marker, name, leafletScope, layers, map); + }, isDeepWatch); + }, + + string: _string, + log: _log, + }; +}]); + +angular.module('leaflet-directive').factory('leafletPathsHelpers', ["$rootScope", "$log", "leafletHelpers", function($rootScope, $log, leafletHelpers) { + var isDefined = leafletHelpers.isDefined; + var isArray = leafletHelpers.isArray; + var isNumber = leafletHelpers.isNumber; + var isValidPoint = leafletHelpers.isValidPoint; + + var availableOptions = [ + + // Path options + 'stroke', 'weight', 'color', 'opacity', + 'fill', 'fillColor', 'fillOpacity', + 'dashArray', 'lineCap', 'lineJoin', 'clickable', + 'pointerEvents', 'className', + + // Polyline options + 'smoothFactor', 'noClip', + ]; + function _convertToLeafletLatLngs(latlngs) { + return latlngs.filter(function(latlng) { + return isValidPoint(latlng); + }).map(function(latlng) { + return _convertToLeafletLatLng(latlng); + }); + } + + function _convertToLeafletLatLng(latlng) { + if (isArray(latlng)) { + return new L.LatLng(latlng[0], latlng[1]); + } else { + return new L.LatLng(latlng.lat, latlng.lng); + } + } + + function _convertToLeafletMultiLatLngs(paths) { + return paths.map(function(latlngs) { + return _convertToLeafletLatLngs(latlngs); + }); + } + + function _getOptions(path, defaults) { + var options = {}; + for (var i = 0; i < availableOptions.length; i++) { + var optionName = availableOptions[i]; + + if (isDefined(path[optionName])) { + options[optionName] = path[optionName]; + } else if (isDefined(defaults.path[optionName])) { + options[optionName] = defaults.path[optionName]; + } + } + + return options; + } + + var _updatePathOptions = function(path, data) { + var updatedStyle = {}; + for (var i = 0; i < availableOptions.length; i++) { + var optionName = availableOptions[i]; + if (isDefined(data[optionName])) { + updatedStyle[optionName] = data[optionName]; + } + } + + path.setStyle(data); + }; + + var _isValidPolyline = function(latlngs) { + if (!isArray(latlngs)) { + return false; + } + + for (var i = 0; i < latlngs.length; i++) { + var point = latlngs[i]; + if (!isValidPoint(point)) { + return false; + } + } + + return true; + }; + + var pathTypes = { + polyline: { + isValid: function(pathData) { + var latlngs = pathData.latlngs; + return _isValidPolyline(latlngs); + }, + + createPath: function(options) { + return new L.Polyline([], options); + }, + + setPath: function(path, data) { + path.setLatLngs(_convertToLeafletLatLngs(data.latlngs)); + _updatePathOptions(path, data); + return; + }, + }, + multiPolyline: { + isValid: function(pathData) { + var latlngs = pathData.latlngs; + if (!isArray(latlngs)) { + return false; + } + + for (var i in latlngs) { + var polyline = latlngs[i]; + if (!_isValidPolyline(polyline)) { + return false; + } + } + + return true; + }, + + createPath: function(options) { + return new L.multiPolyline([[[0, 0], [1, 1]]], options); + }, + + setPath: function(path, data) { + path.setLatLngs(_convertToLeafletMultiLatLngs(data.latlngs)); + _updatePathOptions(path, data); + return; + }, + }, + polygon: { + isValid: function(pathData) { + var latlngs = pathData.latlngs; + return _isValidPolyline(latlngs); + }, + + createPath: function(options) { + return new L.Polygon([], options); + }, + + setPath: function(path, data) { + path.setLatLngs(_convertToLeafletLatLngs(data.latlngs)); + _updatePathOptions(path, data); + return; + }, + }, + multiPolygon: { + isValid: function(pathData) { + var latlngs = pathData.latlngs; + + if (!isArray(latlngs)) { + return false; + } + + for (var i in latlngs) { + var polyline = latlngs[i]; + if (!_isValidPolyline(polyline)) { + return false; + } + } + + return true; + }, + + createPath: function(options) { + return new L.MultiPolygon([[[0, 0], [1, 1], [0, 1]]], options); + }, + + setPath: function(path, data) { + path.setLatLngs(_convertToLeafletMultiLatLngs(data.latlngs)); + _updatePathOptions(path, data); + return; + }, + }, + rectangle: { + isValid: function(pathData) { + var latlngs = pathData.latlngs; + + if (!isArray(latlngs) || latlngs.length !== 2) { + return false; + } + + for (var i in latlngs) { + var point = latlngs[i]; + if (!isValidPoint(point)) { + return false; + } + } + + return true; + }, + + createPath: function(options) { + return new L.Rectangle([[0, 0], [1, 1]], options); + }, + + setPath: function(path, data) { + path.setBounds(new L.LatLngBounds(_convertToLeafletLatLngs(data.latlngs))); + _updatePathOptions(path, data); + }, + }, + circle: { + isValid: function(pathData) { + var point = pathData.latlngs; + return isValidPoint(point) && isNumber(pathData.radius); + }, + + createPath: function(options) { + return new L.Circle([0, 0], 1, options); + }, + + setPath: function(path, data) { + path.setLatLng(_convertToLeafletLatLng(data.latlngs)); + if (isDefined(data.radius)) { + path.setRadius(data.radius); + } + + _updatePathOptions(path, data); + }, + }, + circleMarker: { + isValid: function(pathData) { + var point = pathData.latlngs; + return isValidPoint(point) && isNumber(pathData.radius); + }, + + createPath: function(options) { + return new L.CircleMarker([0, 0], options); + }, + + setPath: function(path, data) { + path.setLatLng(_convertToLeafletLatLng(data.latlngs)); + if (isDefined(data.radius)) { + path.setRadius(data.radius); + } + + _updatePathOptions(path, data); + }, + }, + }; + + var _getPathData = function(path) { + var pathData = {}; + if (path.latlngs) { + pathData.latlngs = path.latlngs; + } + + if (path.radius) { + pathData.radius = path.radius; + } + + return pathData; + }; + + return { + setPathOptions: function(leafletPath, pathType, data) { + if (!isDefined(pathType)) { + pathType = 'polyline'; + } + + pathTypes[pathType].setPath(leafletPath, data); + }, + + createPath: function(name, path, defaults) { + if (!isDefined(path.type)) { + path.type = 'polyline'; + } + + var options = _getOptions(path, defaults); + var pathData = _getPathData(path); + + if (!pathTypes[path.type].isValid(pathData)) { + $log.error('[AngularJS - Leaflet] Invalid data passed to the ' + path.type + ' path'); + return; + } + + return pathTypes[path.type].createPath(options); + }, + }; +}]); + +angular.module('leaflet-directive') +.service('leafletWatchHelpers', function() { + + var _maybe = function(scope, watchFunctionName, thingToWatchStr, watchOptions, initCb) { + //watchOptions.isDeep is/should be ignored in $watchCollection + var unWatch = scope[watchFunctionName](thingToWatchStr, function(newValue, oldValue) { + initCb(newValue, oldValue); + if (!watchOptions.doWatch) + unWatch(); + }, watchOptions.isDeep); + + return unWatch; + }; + + /* + @name: maybeWatch + @description: Utility to watch something once or forever. + @returns unWatch function + @param watchOptions - see markersWatchOptions and or derrivatives. This object is used + to set watching to once and its watch depth. + */ + var _maybeWatch = function(scope, thingToWatchStr, watchOptions, initCb) { + return _maybe(scope, '$watch', thingToWatchStr, watchOptions, initCb); + }; + + /* + @name: _maybeWatchCollection + @description: Utility to watch something once or forever. + @returns unWatch function + @param watchOptions - see markersWatchOptions and or derrivatives. This object is used + to set watching to once and its watch depth. + */ + var _maybeWatchCollection = function(scope, thingToWatchStr, watchOptions, initCb) { + return _maybe(scope, '$watchCollection', thingToWatchStr, watchOptions, initCb); + }; + + return { + maybeWatch: _maybeWatch, + maybeWatchCollection: _maybeWatchCollection, + }; +}); + +angular.module('leaflet-directive').factory('nominatimService', ["$q", "$http", "leafletHelpers", "leafletMapDefaults", function($q, $http, leafletHelpers, leafletMapDefaults) { + var isDefined = leafletHelpers.isDefined; + + return { + query: function(address, mapId) { + var defaults = leafletMapDefaults.getDefaults(mapId); + var url = defaults.nominatim.server; + var df = $q.defer(); + + $http.get(url, { params: { format: 'json', limit: 1, q: address } }).success(function(data) { + if (data.length > 0 && isDefined(data[0].boundingbox)) { + df.resolve(data[0]); + } else { + df.reject('[Nominatim] Invalid address'); + } + }); + + return df.promise; + }, + }; +}]); + +angular.module('leaflet-directive').directive('bounds', ["$log", "$timeout", "$http", "leafletHelpers", "nominatimService", "leafletBoundsHelpers", function($log, $timeout, $http, leafletHelpers, nominatimService, leafletBoundsHelpers) { + + return { + restrict: 'A', + scope: false, + replace: false, + require: ['leaflet'], + + link: function(scope, element, attrs, controller) { + var isDefined = leafletHelpers.isDefined; + var createLeafletBounds = leafletBoundsHelpers.createLeafletBounds; + var leafletScope = controller[0].getLeafletScope(); + var mapController = controller[0]; + var errorHeader = leafletHelpers.errorHeader + ' [Bounds] '; + + var emptyBounds = function(bounds) { + return (bounds._southWest.lat === 0 && bounds._southWest.lng === 0 && + bounds._northEast.lat === 0 && bounds._northEast.lng === 0); + }; + + mapController.getMap().then(function(map) { + leafletScope.$on('boundsChanged', function(event) { + var scope = event.currentScope; + var bounds = map.getBounds(); + + if (emptyBounds(bounds) || scope.settingBoundsFromScope) { + return; + } + + scope.settingBoundsFromLeaflet = true; + var newScopeBounds = { + northEast: { + lat: bounds._northEast.lat, + lng: bounds._northEast.lng, + }, + southWest: { + lat: bounds._southWest.lat, + lng: bounds._southWest.lng, + }, + options: bounds.options, + }; + if (!angular.equals(scope.bounds, newScopeBounds)) { + scope.bounds = newScopeBounds; + } + + $timeout(function() { + scope.settingBoundsFromLeaflet = false; + }); + }); + + var lastNominatimQuery; + leafletScope.$watch('bounds', function(bounds) { + if (scope.settingBoundsFromLeaflet) + return; + if (isDefined(bounds.address) && bounds.address !== lastNominatimQuery) { + scope.settingBoundsFromScope = true; + nominatimService.query(bounds.address, attrs.id).then(function(data) { + var b = data.boundingbox; + var newBounds = [[b[0], b[2]], [b[1], b[3]]]; + map.fitBounds(newBounds); + }, function(errMsg) { + + $log.error(errorHeader + ' ' + errMsg + '.'); + }); + + lastNominatimQuery = bounds.address; + $timeout(function() { + scope.settingBoundsFromScope = false; + }); + + return; + } + + var leafletBounds = createLeafletBounds(bounds); + if (leafletBounds && !map.getBounds().equals(leafletBounds)) { + scope.settingBoundsFromScope = true; + map.fitBounds(leafletBounds, bounds.options); + $timeout(function() { + scope.settingBoundsFromScope = false; + }); + } + }, true); + }); + }, + }; +}]); + +var centerDirectiveTypes = ['center', 'lfCenter']; +var centerDirectives = {}; + +centerDirectiveTypes.forEach(function(directiveName) { + centerDirectives[directiveName] = ['$log', '$q', '$location', '$timeout', 'leafletMapDefaults', 'leafletHelpers', + 'leafletBoundsHelpers', 'leafletMapEvents', + function($log, $q, $location, $timeout, leafletMapDefaults, leafletHelpers, + leafletBoundsHelpers, leafletMapEvents) { + + var isDefined = leafletHelpers.isDefined; + var isNumber = leafletHelpers.isNumber; + var isSameCenterOnMap = leafletHelpers.isSameCenterOnMap; + var safeApply = leafletHelpers.safeApply; + var isValidCenter = leafletHelpers.isValidCenter; + var isValidBounds = leafletBoundsHelpers.isValidBounds; + var isUndefinedOrEmpty = leafletHelpers.isUndefinedOrEmpty; + var errorHeader = leafletHelpers.errorHeader; + + var shouldInitializeMapWithBounds = function(bounds, center) { + return isDefined(bounds) && isValidBounds(bounds) && isUndefinedOrEmpty(center); + }; + var _leafletCenter; return { restrict: 'A', scope: false, replace: false, require: 'leaflet', - controller: function () { + controller: function() { _leafletCenter = $q.defer(); - this.getCenter = function () { + this.getCenter = function() { return _leafletCenter.promise; }; }, - link: function (scope, element, attrs, controller) { - var leafletScope = controller.getLeafletScope(), centerModel = leafletScope.center; - controller.getMap().then(function (map) { + + link: function(scope, element, attrs, controller) { + var leafletScope = controller.getLeafletScope(); + var centerModel = leafletScope[directiveName]; + + controller.getMap().then(function(map) { var defaults = leafletMapDefaults.getDefaults(attrs.id); - if (attrs.center.search('-') !== -1) { - $log.error('The "center" variable can\'t use a "-" on his key name: "' + attrs.center + '".'); - map.setView([ - defaults.center.lat, - defaults.center.lng - ], defaults.center.zoom); + + if (attrs[directiveName].search('-') !== -1) { + $log.error(errorHeader + ' The "center" variable can\'t use a "-" on its key name: "' + attrs[directiveName] + '".'); + map.setView([defaults.center.lat, defaults.center.lng], defaults.center.zoom); return; } else if (shouldInitializeMapWithBounds(leafletScope.bounds, centerModel)) { - map.fitBounds(leafletBoundsHelpers.createLeafletBounds(leafletScope.bounds)); + map.fitBounds(leafletBoundsHelpers.createLeafletBounds(leafletScope.bounds), leafletScope.bounds.options); centerModel = map.getCenter(); - safeApply(leafletScope, function (scope) { - scope.center = { + safeApply(leafletScope, function(scope) { + angular.extend(scope[directiveName], { lat: map.getCenter().lat, lng: map.getCenter().lng, zoom: map.getZoom(), - autoDiscover: false + autoDiscover: false, + }); + }); + + safeApply(leafletScope, function(scope) { + var mapBounds = map.getBounds(); + scope.bounds = { + northEast: { + lat: mapBounds._northEast.lat, + lng: mapBounds._northEast.lng, + }, + southWest: { + lat: mapBounds._southWest.lat, + lng: mapBounds._southWest.lng, + }, }; }); - safeApply(leafletScope, function (scope) { - var mapBounds = map.getBounds(); - var newScopeBounds = { - northEast: { - lat: mapBounds._northEast.lat, - lng: mapBounds._northEast.lng - }, - southWest: { - lat: mapBounds._southWest.lat, - lng: mapBounds._southWest.lng - } - }; - scope.bounds = newScopeBounds; - }); } else if (!isDefined(centerModel)) { - $log.error('The "center" property is not defined in the main scope'); - map.setView([ - defaults.center.lat, - defaults.center.lng - ], defaults.center.zoom); + $log.error(errorHeader + ' The "center" property is not defined in the main scope'); + map.setView([defaults.center.lat, defaults.center.lng], defaults.center.zoom); return; } else if (!(isDefined(centerModel.lat) && isDefined(centerModel.lng)) && !isDefined(centerModel.autoDiscover)) { angular.copy(defaults.center, centerModel); } - var urlCenterHash, mapReady; + + var urlCenterHash; + var mapReady; if (attrs.urlHashCenter === 'yes') { - var extractCenterFromUrl = function () { + var extractCenterFromUrl = function() { var search = $location.search(); var centerParam; if (isDefined(search.c)) { @@ -181,2696 +3453,2282 @@ centerParam = { lat: parseFloat(cParam[0]), lng: parseFloat(cParam[1]), - zoom: parseInt(cParam[2], 10) + zoom: parseInt(cParam[2], 10), }; } } + return centerParam; }; + urlCenterHash = extractCenterFromUrl(); - leafletScope.$on('$locationChangeSuccess', function (event) { + + leafletScope.$on('$locationChangeSuccess', function(event) { var scope = event.currentScope; + //$log.debug("updated location..."); var urlCenter = extractCenterFromUrl(); if (isDefined(urlCenter) && !isSameCenterOnMap(urlCenter, map)) { //$log.debug("updating center model...", urlCenter); - scope.center = { + angular.extend(scope[directiveName], { lat: urlCenter.lat, lng: urlCenter.lng, - zoom: urlCenter.zoom - }; + zoom: urlCenter.zoom, + }); } }); } - leafletScope.$watch('center', function (center) { + + leafletScope.$watch(directiveName, function(center) { + if (leafletScope.settingCenterFromLeaflet) + return; + //$log.debug("updated center model..."); // The center from the URL has priority if (isDefined(urlCenterHash)) { angular.copy(urlCenterHash, center); urlCenterHash = undefined; } + if (!isValidCenter(center) && center.autoDiscover !== true) { - $log.warn('[AngularJS - Leaflet] invalid \'center\''); + $log.warn(errorHeader + ' invalid \'center\''); + //map.setView([defaults.center.lat, defaults.center.lng], defaults.center.zoom); return; } + if (center.autoDiscover === true) { if (!isNumber(center.zoom)) { - map.setView([ - defaults.center.lat, - defaults.center.lng - ], defaults.center.zoom); + map.setView([defaults.center.lat, defaults.center.lng], defaults.center.zoom); } + if (isNumber(center.zoom) && center.zoom > defaults.center.zoom) { map.locate({ setView: true, - maxZoom: center.zoom + maxZoom: center.zoom, }); } else if (isDefined(defaults.maxZoom)) { map.locate({ setView: true, - maxZoom: defaults.maxZoom + maxZoom: defaults.maxZoom, }); } else { - map.locate({ setView: true }); + map.locate({ + setView: true, + }); } + return; } + if (mapReady && isSameCenterOnMap(center, map)) { //$log.debug("no need to update map again."); return; } + //$log.debug("updating map center...", center); - map.setView([ - center.lat, - center.lng - ], center.zoom); - leafletEvents.notifyCenterChangedToBounds(leafletScope, map); + leafletScope.settingCenterFromScope = true; + map.setView([center.lat, center.lng], center.zoom); + leafletMapEvents.notifyCenterChangedToBounds(leafletScope, map); + $timeout(function() { + leafletScope.settingCenterFromScope = false; + + //$log.debug("allow center scope updates"); + }); }, true); - map.whenReady(function () { + + map.whenReady(function() { mapReady = true; }); - map.on('moveend', function () { + + map.on('moveend', function(/* event */) { // Resolve the center after the first map position _leafletCenter.resolve(); - leafletEvents.notifyCenterUrlHashChanged(leafletScope, map, attrs, $location.search()); + leafletMapEvents.notifyCenterUrlHashChanged(leafletScope, map, attrs, $location.search()); + //$log.debug("updated center on map..."); - if (isSameCenterOnMap(centerModel, map)) { + if (isSameCenterOnMap(centerModel, map) || leafletScope.settingCenterFromScope) { //$log.debug("same center in model, no need to update again."); return; } - safeApply(leafletScope, function (scope) { - //$log.debug("updating center model...", map.getCenter(), map.getZoom()); - scope.center = { - lat: map.getCenter().lat, - lng: map.getCenter().lng, - zoom: map.getZoom(), - autoDiscover: false - }; - leafletEvents.notifyCenterChangedToBounds(leafletScope, map); - }); - }); - if (centerModel.autoDiscover === true) { - map.on('locationerror', function () { - $log.warn('[AngularJS - Leaflet] The Geolocation API is unauthorized on this page.'); - if (isValidCenter(centerModel)) { - map.setView([ - centerModel.lat, - centerModel.lng - ], centerModel.zoom); - leafletEvents.notifyCenterChangedToBounds(leafletScope, map); - } else { - map.setView([ - defaults.center.lat, - defaults.center.lng - ], defaults.center.zoom); - leafletEvents.notifyCenterChangedToBounds(leafletScope, map); - } - }); - } - }); - } - }; - } - ]); - angular.module('leaflet-directive').directive('tiles', [ - '$log', - 'leafletData', - 'leafletMapDefaults', - 'leafletHelpers', - function ($log, leafletData, leafletMapDefaults, leafletHelpers) { - return { - restrict: 'A', - scope: false, - replace: false, - require: 'leaflet', - link: function (scope, element, attrs, controller) { - var isDefined = leafletHelpers.isDefined, leafletScope = controller.getLeafletScope(), tiles = leafletScope.tiles; - if (!isDefined(tiles) && !isDefined(tiles.url)) { - $log.warn('[AngularJS - Leaflet] The \'tiles\' definition doesn\'t have the \'url\' property.'); - return; - } - controller.getMap().then(function (map) { - var defaults = leafletMapDefaults.getDefaults(attrs.id); - var tileLayerObj; - leafletScope.$watch('tiles', function (tiles) { - var tileLayerOptions = defaults.tileLayerOptions; - var tileLayerUrl = defaults.tileLayer; - // If no valid tiles are in the scope, remove the last layer - if (!isDefined(tiles.url) && isDefined(tileLayerObj)) { - map.removeLayer(tileLayerObj); - return; - } - // No leafletTiles object defined yet - if (!isDefined(tileLayerObj)) { - if (isDefined(tiles.options)) { - angular.copy(tiles.options, tileLayerOptions); - } - if (isDefined(tiles.url)) { - tileLayerUrl = tiles.url; - } - tileLayerObj = L.tileLayer(tileLayerUrl, tileLayerOptions); - tileLayerObj.addTo(map); - leafletData.setTiles(tileLayerObj, attrs.id); - return; - } - // If the options of the tilelayer is changed, we need to redraw the layer - if (isDefined(tiles.url) && isDefined(tiles.options) && !angular.equals(tiles.options, tileLayerOptions)) { - map.removeLayer(tileLayerObj); - tileLayerOptions = defaults.tileLayerOptions; - angular.copy(tiles.options, tileLayerOptions); - tileLayerUrl = tiles.url; - tileLayerObj = L.tileLayer(tileLayerUrl, tileLayerOptions); - tileLayerObj.addTo(map); - leafletData.setTiles(tileLayerObj, attrs.id); - return; - } - // Only the URL of the layer is changed, update the tiles object - if (isDefined(tiles.url)) { - tileLayerObj.setUrl(tiles.url); - } - }, true); - }); - } - }; - } - ]); - angular.module('leaflet-directive').directive('legend', [ - '$log', - '$http', - 'leafletHelpers', - 'leafletLegendHelpers', - function ($log, $http, leafletHelpers, leafletLegendHelpers) { - return { - restrict: 'A', - scope: false, - replace: false, - require: 'leaflet', - link: function (scope, element, attrs, controller) { - var isArray = leafletHelpers.isArray, isDefined = leafletHelpers.isDefined, isFunction = leafletHelpers.isFunction, leafletScope = controller.getLeafletScope(), legend = leafletScope.legend; - var legendClass = legend.legendClass ? legend.legendClass : 'legend'; - var position = legend.position || 'bottomright'; - var leafletLegend; - controller.getMap().then(function (map) { - if (!isDefined(legend.url) && (!isArray(legend.colors) || !isArray(legend.labels) || legend.colors.length !== legend.labels.length)) { - $log.warn('[AngularJS - Leaflet] legend.colors and legend.labels must be set.'); - } else if (isDefined(legend.url)) { - $log.info('[AngularJS - Leaflet] loading arcgis legend service.'); - } else { - // TODO: Watch array legend. - leafletLegend = L.control({ position: position }); - leafletLegend.onAdd = leafletLegendHelpers.getOnAddArrayLegend(legend, legendClass); - leafletLegend.addTo(map); - } - leafletScope.$watch('legend.url', function (newURL) { - if (!isDefined(newURL)) { - return; - } - $http.get(newURL).success(function (legendData) { - if (isDefined(leafletLegend)) { - leafletLegendHelpers.updateArcGISLegend(leafletLegend.getContainer(), legendData); - } else { - leafletLegend = L.control({ position: position }); - leafletLegend.onAdd = leafletLegendHelpers.getOnAddArcGISLegend(legendData, legendClass); - leafletLegend.addTo(map); - } - if (isDefined(legend.loadedData) && isFunction(legend.loadedData)) { - legend.loadedData(); - } - }).error(function () { - $log.warn('[AngularJS - Leaflet] legend.url not loaded.'); - }); - }); - }); - } - }; - } - ]); - angular.module('leaflet-directive').directive('geojson', [ - '$log', - '$rootScope', - 'leafletData', - 'leafletHelpers', - function ($log, $rootScope, leafletData, leafletHelpers) { - return { - restrict: 'A', - scope: false, - replace: false, - require: 'leaflet', - link: function (scope, element, attrs, controller) { - var safeApply = leafletHelpers.safeApply, isDefined = leafletHelpers.isDefined, leafletScope = controller.getLeafletScope(), leafletGeoJSON = {}; - controller.getMap().then(function (map) { - leafletScope.$watch('geojson', function (geojson) { - if (isDefined(leafletGeoJSON) && map.hasLayer(leafletGeoJSON)) { - map.removeLayer(leafletGeoJSON); - } - if (!(isDefined(geojson) && isDefined(geojson.data))) { - return; - } - var resetStyleOnMouseout = geojson.resetStyleOnMouseout, onEachFeature = geojson.onEachFeature; - if (!onEachFeature) { - onEachFeature = function (feature, layer) { - if (leafletHelpers.LabelPlugin.isLoaded() && isDefined(geojson.label)) { - layer.bindLabel(feature.properties.description); - } - layer.on({ - mouseover: function (e) { - safeApply(leafletScope, function () { - geojson.selected = feature; - $rootScope.$broadcast('leafletDirectiveMap.geojsonMouseover', e); - }); - }, - mouseout: function (e) { - if (resetStyleOnMouseout) { - leafletGeoJSON.resetStyle(e.target); - } - safeApply(leafletScope, function () { - geojson.selected = undefined; - $rootScope.$broadcast('leafletDirectiveMap.geojsonMouseout', e); - }); - }, - click: function (e) { - safeApply(leafletScope, function () { - geojson.selected = feature; - $rootScope.$broadcast('leafletDirectiveMap.geojsonClick', geojson.selected, e); - }); - } + + leafletScope.settingCenterFromLeaflet = true; + safeApply(leafletScope, function(scope) { + if (!leafletScope.settingCenterFromScope) { + //$log.debug("updating center model...", map.getCenter(), map.getZoom()); + angular.extend(scope[directiveName], { + lat: map.getCenter().lat, + lng: map.getCenter().lng, + zoom: map.getZoom(), + autoDiscover: false, }); - }; - } - geojson.options = { - style: geojson.style, - filter: geojson.filter, - onEachFeature: onEachFeature, - pointToLayer: geojson.pointToLayer - }; - leafletGeoJSON = L.geoJson(geojson.data, geojson.options); - leafletData.setGeoJSON(leafletGeoJSON); - leafletGeoJSON.addTo(map); + } + + leafletMapEvents.notifyCenterChangedToBounds(leafletScope, map); + $timeout(function() { + leafletScope.settingCenterFromLeaflet = false; + }); + }); }); + + if (centerModel.autoDiscover === true) { + map.on('locationerror', function() { + $log.warn(errorHeader + ' The Geolocation API is unauthorized on this page.'); + if (isValidCenter(centerModel)) { + map.setView([centerModel.lat, centerModel.lng], centerModel.zoom); + leafletMapEvents.notifyCenterChangedToBounds(leafletScope, map); + } else { + map.setView([defaults.center.lat, defaults.center.lng], defaults.center.zoom); + leafletMapEvents.notifyCenterChangedToBounds(leafletScope, map); + } + }); + } }); - } - }; - } - ]); - angular.module('leaflet-directive').directive('layers', [ - '$log', - '$q', - 'leafletData', - 'leafletHelpers', - 'leafletLayerHelpers', - 'leafletControlHelpers', - function ($log, $q, leafletData, leafletHelpers, leafletLayerHelpers, leafletControlHelpers) { - var _leafletLayers; - return { - restrict: 'A', - scope: false, - replace: false, - require: 'leaflet', - controller: function () { - _leafletLayers = $q.defer(); - this.getLayers = function () { - return _leafletLayers.promise; - }; }, - link: function (scope, element, attrs, controller) { - var isDefined = leafletHelpers.isDefined, leafletLayers = {}, leafletScope = controller.getLeafletScope(), layers = leafletScope.layers, createLayer = leafletLayerHelpers.createLayer, updateLayersControl = leafletControlHelpers.updateLayersControl, isLayersControlVisible = false; - controller.getMap().then(function (map) { - // Do we have a baselayers property? - if (!isDefined(layers) || !isDefined(layers.baselayers) || Object.keys(layers.baselayers).length === 0) { - // No baselayers property - $log.error('[AngularJS - Leaflet] At least one baselayer has to be defined'); + }; + }, + ]; +}); + +centerDirectiveTypes.forEach(function(dirType) { + angular.module('leaflet-directive').directive(dirType, centerDirectives[dirType]); +}); + +angular.module('leaflet-directive').directive('controls', ["$log", "leafletHelpers", "leafletControlHelpers", function($log, leafletHelpers, leafletControlHelpers) { + + return { + restrict: 'A', + scope: false, + replace: false, + require: '?^leaflet', + + link: function(scope, element, attrs, controller) { + if (!controller) { + return; + } + + var createControl = leafletControlHelpers.createControl; + var isValidControlType = leafletControlHelpers.isValidControlType; + var leafletScope = controller.getLeafletScope(); + var isDefined = leafletHelpers.isDefined; + var isArray = leafletHelpers.isArray; + var leafletControls = {}; + var errorHeader = leafletHelpers.errorHeader + ' [Controls] '; + + controller.getMap().then(function(map) { + + leafletScope.$watchCollection('controls', function(newControls) { + + // Delete controls from the array + for (var name in leafletControls) { + if (!isDefined(newControls[name])) { + if (map.hasControl(leafletControls[name])) { + map.removeControl(leafletControls[name]); + } + + delete leafletControls[name]; + } + } + + for (var newName in newControls) { + var control; + + var controlType = isDefined(newControls[newName].type) ? newControls[newName].type : newName; + + if (!isValidControlType(controlType)) { + $log.error(errorHeader + ' Invalid control type: ' + controlType + '.'); return; } - // We have baselayers to add to the map - _leafletLayers.resolve(leafletLayers); - leafletData.setLayers(leafletLayers, attrs.id); - leafletLayers.baselayers = {}; - leafletLayers.overlays = {}; - var mapId = attrs.id; - // Setup all baselayers definitions - var oneVisibleLayer = false; - for (var layerName in layers.baselayers) { - var newBaseLayer = createLayer(layers.baselayers[layerName]); - if (!isDefined(newBaseLayer)) { - delete layers.baselayers[layerName]; - continue; - } - leafletLayers.baselayers[layerName] = newBaseLayer; - // Only add the visible layer to the map, layer control manages the addition to the map - // of layers in its control - if (layers.baselayers[layerName].top === true) { - map.addLayer(leafletLayers.baselayers[layerName]); - oneVisibleLayer = true; - } - } - // If there is no visible layer add first to the map - if (!oneVisibleLayer && Object.keys(leafletLayers.baselayers).length > 0) { - map.addLayer(leafletLayers.baselayers[Object.keys(layers.baselayers)[0]]); - } - // Setup the Overlays - for (layerName in layers.overlays) { - var newOverlayLayer = createLayer(layers.overlays[layerName]); - if (!isDefined(newOverlayLayer)) { - delete layers.overlays[layerName]; - continue; - } - leafletLayers.overlays[layerName] = newOverlayLayer; - // Only add the visible overlays to the map - if (layers.overlays[layerName].visible === true) { - map.addLayer(leafletLayers.overlays[layerName]); - } - } - // Watch for the base layers - leafletScope.$watch('layers.baselayers', function (newBaseLayers) { - // Delete layers from the array - for (var name in leafletLayers.baselayers) { - if (!isDefined(newBaseLayers[name])) { - // Remove from the map if it's on it - if (map.hasLayer(leafletLayers.baselayers[name])) { - map.removeLayer(leafletLayers.baselayers[name]); - } - delete leafletLayers.baselayers[name]; + + if (controlType !== 'custom') { + control = createControl(controlType, newControls[newName]); + map.addControl(control); + leafletControls[newName] = control; + } else { + var customControlValue = newControls[newName]; + if (isArray(customControlValue)) { + for (var i in customControlValue) { + var customControl = customControlValue[i]; + map.addControl(customControl); + leafletControls[newName] = !isDefined(leafletControls[newName]) ? [customControl] : leafletControls[newName].concat([customControl]); } + } else { + map.addControl(customControlValue); + leafletControls[newName] = customControlValue; } - // add new layers - for (var newName in newBaseLayers) { - if (!isDefined(leafletLayers.baselayers[newName])) { - var testBaseLayer = createLayer(newBaseLayers[newName]); - if (isDefined(testBaseLayer)) { - leafletLayers.baselayers[newName] = testBaseLayer; - // Only add the visible layer to the map - if (newBaseLayers[newName].top === true) { - map.addLayer(leafletLayers.baselayers[newName]); - } - } - } + } + } + + }); + + }); + }, + }; +}]); + +angular.module('leaflet-directive').directive('decorations', ["$log", "leafletHelpers", function($log, leafletHelpers) { + + return { + restrict: 'A', + scope: false, + replace: false, + require: 'leaflet', + + link: function(scope, element, attrs, controller) { + var leafletScope = controller.getLeafletScope(); + var PolylineDecoratorPlugin = leafletHelpers.PolylineDecoratorPlugin; + var isDefined = leafletHelpers.isDefined; + var leafletDecorations = {}; + + /* Creates an "empty" decoration with a set of coordinates, but no pattern. */ + function createDecoration(options) { + if (isDefined(options) && isDefined(options.coordinates)) { + if (!PolylineDecoratorPlugin.isLoaded()) { + $log.error('[AngularJS - Leaflet] The PolylineDecorator Plugin is not loaded.'); + } + } + + return L.polylineDecorator(options.coordinates); + } + + /* Updates the path and the patterns for the provided decoration, and returns the decoration. */ + function setDecorationOptions(decoration, options) { + if (isDefined(decoration) && isDefined(options)) { + if (isDefined(options.coordinates) && isDefined(options.patterns)) { + decoration.setPaths(options.coordinates); + decoration.setPatterns(options.patterns); + return decoration; + } + } + } + + controller.getMap().then(function(map) { + leafletScope.$watch('decorations', function(newDecorations) { + for (var name in leafletDecorations) { + if (!isDefined(newDecorations[name]) || !angular.equals(newDecorations[name], leafletDecorations)) { + map.removeLayer(leafletDecorations[name]); + delete leafletDecorations[name]; + } + } + + for (var newName in newDecorations) { + var decorationData = newDecorations[newName]; + var newDecoration = createDecoration(decorationData); + + if (isDefined(newDecoration)) { + leafletDecorations[newName] = newDecoration; + map.addLayer(newDecoration); + setDecorationOptions(newDecoration, decorationData); + } + } + }, true); + }); + }, + }; +}]); + +angular.module('leaflet-directive').directive('eventBroadcast', ["$log", "$rootScope", "leafletHelpers", "leafletMapEvents", "leafletIterators", function($log, $rootScope, leafletHelpers, leafletMapEvents, leafletIterators) { + + return { + restrict: 'A', + scope: false, + replace: false, + require: 'leaflet', + + link: function(scope, element, attrs, controller) { + var isObject = leafletHelpers.isObject; + var isDefined = leafletHelpers.isDefined; + var leafletScope = controller.getLeafletScope(); + var eventBroadcast = leafletScope.eventBroadcast; + var availableMapEvents = leafletMapEvents.getAvailableMapEvents(); + var addEvents = leafletMapEvents.addEvents; + + controller.getMap().then(function(map) { + + var mapEvents = []; + var logic = 'broadcast'; + + // We have a possible valid object + if (!isDefined(eventBroadcast.map)) { + // We do not have events enable/disable do we do nothing (all enabled by default) + mapEvents = availableMapEvents; + } else if (!isObject(eventBroadcast.map)) { + // Not a valid object + $log.warn('[AngularJS - Leaflet] event-broadcast.map must be an object check your model.'); + } else { + // We have a possible valid map object + // Event propadation logic + if (eventBroadcast.map.logic !== 'emit' && eventBroadcast.map.logic !== 'broadcast') { + // This is an error + $log.warn('[AngularJS - Leaflet] Available event propagation logic are: \'emit\' or \'broadcast\'.'); + } else { + logic = eventBroadcast.map.logic; + } + + if (!(isObject(eventBroadcast.map.enable) && eventBroadcast.map.enable.length >= 0)) { + $log.warn('[AngularJS - Leaflet] event-broadcast.map.enable must be an object check your model.'); + } else { + // Enable events + leafletIterators.each(eventBroadcast.map.enable, function(eventName) { + // Do we have already the event enabled? + if (mapEvents.indexOf(eventName) === -1 && availableMapEvents.indexOf(eventName) !== -1) { + mapEvents.push(eventName); } - if (Object.keys(leafletLayers.baselayers).length === 0) { - $log.error('[AngularJS - Leaflet] At least one baselayer has to be defined'); + }); + } + + } + + // as long as the map is removed in the root leaflet directive we + // do not need ot clean up the events as leaflet does it itself + addEvents(map, mapEvents, 'eventName', leafletScope, logic); + }); + }, + }; +}]); + +angular.module('leaflet-directive') +.directive('geojson', ["$log", "$rootScope", "leafletData", "leafletHelpers", "leafletWatchHelpers", "leafletDirectiveControlsHelpers", "leafletIterators", "leafletGeoJsonEvents", function($log, $rootScope, leafletData, leafletHelpers, + leafletWatchHelpers, leafletDirectiveControlsHelpers, leafletIterators, leafletGeoJsonEvents) { + var _maybeWatch = leafletWatchHelpers.maybeWatch; + var _watchOptions = leafletHelpers.watchOptions; + var _extendDirectiveControls = leafletDirectiveControlsHelpers.extend; + var hlp = leafletHelpers; + var $it = leafletIterators; + + return { + restrict: 'A', + scope: false, + replace: false, + require: 'leaflet', + + link: function(scope, element, attrs, controller) { + var isDefined = leafletHelpers.isDefined; + var leafletScope = controller.getLeafletScope(); + var leafletGeoJSON = {}; + var _hasSetLeafletData = false; + + controller.getMap().then(function(map) { + var watchOptions = leafletScope.geojsonWatchOptions || _watchOptions; + + var _hookUpEvents = function(geojson, maybeName) { + var onEachFeature; + + if (angular.isFunction(geojson.onEachFeature)) { + onEachFeature = geojson.onEachFeature; + } else { + onEachFeature = function(feature, layer) { + if (leafletHelpers.LabelPlugin.isLoaded() && isDefined(feature.properties.description)) { + layer.bindLabel(feature.properties.description); + } + + leafletGeoJsonEvents.bindEvents(attrs.id, layer, null, feature, + leafletScope, maybeName, + {resetStyleOnMouseout: geojson.resetStyleOnMouseout, + mapId: attrs.id, }); + }; + } + + return onEachFeature; + }; + + var isNested = (hlp.isDefined(attrs.geojsonNested) && + hlp.isTruthy(attrs.geojsonNested)); + + var _clean = function() { + if (!leafletGeoJSON) + return; + var _remove = function(lObject) { + if (isDefined(lObject) && map.hasLayer(lObject)) { + map.removeLayer(lObject); + } + }; + + if (isNested) { + $it.each(leafletGeoJSON, function(lObject) { + _remove(lObject); + }); + + return; + } + + _remove(leafletGeoJSON); + }; + + var _addGeojson = function(model, maybeName) { + var geojson = angular.copy(model); + if (!(isDefined(geojson) && isDefined(geojson.data))) { + return; + } + + var onEachFeature = _hookUpEvents(geojson, maybeName); + + if (!isDefined(geojson.options)) { + //right here is why we use a clone / copy (we modify and thus) + //would kick of a watcher.. we need to be more careful everywhere + //for stuff like this + geojson.options = { + style: geojson.style, + filter: geojson.filter, + onEachFeature: onEachFeature, + pointToLayer: geojson.pointToLayer, + }; + } + + var lObject = L.geoJson(geojson.data, geojson.options); + + if (maybeName && hlp.isString(maybeName)) { + leafletGeoJSON[maybeName] = lObject; + } else { + leafletGeoJSON = lObject; + } + + lObject.addTo(map); + + if (!_hasSetLeafletData) {//only do this once and play with the same ref forever + _hasSetLeafletData = true; + leafletData.setGeoJSON(leafletGeoJSON, attrs.id); + } + }; + + var _create = function(model) { + _clean(); + if (isNested) { + if (!model || !Object.keys(model).length) return; - } - //we have layers, so we need to make, at least, one active - var found = false; - // search for an active layer - for (var key in leafletLayers.baselayers) { + $it.each(model, function(m, name) { + //name could be layerName and or groupName + //for now it is not tied to a layer + _addGeojson(m, name); + }); + + return; + } + + _addGeojson(model); + }; + + _extendDirectiveControls(attrs.id, 'geojson', _create, _clean); + + _maybeWatch(leafletScope, 'geojson', watchOptions, function(geojson) { + _create(geojson); + }); + }); + }, + }; +}]); + +angular.module('leaflet-directive').directive('layercontrol', ["$filter", "$log", "leafletData", "leafletHelpers", function($filter, $log, leafletData, leafletHelpers) { + + return { + restrict: 'E', + scope: { + icons: '=?', + autoHideOpacity: '=?', // Hide other opacity controls when one is activated. + showGroups: '=?', // Hide other opacity controls when one is activated. + title: '@', + baseTitle: '@', + overlaysTitle: '@', + }, + replace: true, + transclude: false, + require: '^leaflet', + controller: ["$scope", "$element", "$sce", function($scope, $element, $sce) { + $log.debug('[Angular Directive - Layers] layers', $scope, $element); + var safeApply = leafletHelpers.safeApply; + var isDefined = leafletHelpers.isDefined; + angular.extend($scope, { + baselayer: '', + oldGroup: '', + layerProperties: {}, + groupProperties: {}, + rangeIsSupported: leafletHelpers.rangeIsSupported(), + changeBaseLayer: function(key, e) { + leafletHelpers.safeApply($scope, function(scp) { + scp.baselayer = key; + leafletData.getMap().then(function(map) { + leafletData.getLayers().then(function(leafletLayers) { if (map.hasLayer(leafletLayers.baselayers[key])) { - found = true; + return; + } + + for (var i in scp.layers.baselayers) { + scp.layers.baselayers[i].icon = scp.icons.unradio; + if (map.hasLayer(leafletLayers.baselayers[i])) { + map.removeLayer(leafletLayers.baselayers[i]); + } + } + + map.addLayer(leafletLayers.baselayers[key]); + scp.layers.baselayers[key].icon = $scope.icons.radio; + }); + }); + }); + + e.preventDefault(); + }, + + moveLayer: function(ly, newIndex, e) { + var delta = Object.keys($scope.layers.baselayers).length; + if (newIndex >= (1 + delta) && newIndex <= ($scope.overlaysArray.length + delta)) { + var oldLy; + for (var key in $scope.layers.overlays) { + if ($scope.layers.overlays[key].index === newIndex) { + oldLy = $scope.layers.overlays[key]; + break; + } + } + + if (oldLy) { + safeApply($scope, function() { + oldLy.index = ly.index; + ly.index = newIndex; + }); + } + } + + e.stopPropagation(); + e.preventDefault(); + }, + + initIndex: function(layer, idx) { + var delta = Object.keys($scope.layers.baselayers).length; + layer.index = isDefined(layer.index) ? layer.index : idx + delta + 1; + }, + + initGroup: function(groupName) { + $scope.groupProperties[groupName] = $scope.groupProperties[groupName] ? $scope.groupProperties[groupName] : {}; + }, + + toggleOpacity: function(e, layer) { + if (layer.visible) { + if ($scope.autoHideOpacity && !$scope.layerProperties[layer.name].opacityControl) { + for (var k in $scope.layerProperties) { + $scope.layerProperties[k].opacityControl = false; + } + } + + $scope.layerProperties[layer.name].opacityControl = !$scope.layerProperties[layer.name].opacityControl; + } + + e.stopPropagation(); + e.preventDefault(); + }, + + toggleLegend: function(layer) { + $scope.layerProperties[layer.name].showLegend = !$scope.layerProperties[layer.name].showLegend; + }, + + showLegend: function(layer) { + return layer.legend && $scope.layerProperties[layer.name].showLegend; + }, + + unsafeHTML: function(html) { + return $sce.trustAsHtml(html); + }, + + getOpacityIcon: function(layer) { + return layer.visible && $scope.layerProperties[layer.name].opacityControl ? $scope.icons.close : $scope.icons.open; + }, + + getGroupIcon: function(group) { + return group.visible ? $scope.icons.check : $scope.icons.uncheck; + }, + + changeOpacity: function(layer) { + var op = $scope.layerProperties[layer.name].opacity; + leafletData.getMap().then(function(map) { + leafletData.getLayers().then(function(leafletLayers) { + var ly; + for (var k in $scope.layers.overlays) { + if ($scope.layers.overlays[k] === layer) { + ly = leafletLayers.overlays[k]; break; } } - // If there is no active layer make one active - if (!found) { - map.addLayer(leafletLayers.baselayers[Object.keys(layers.baselayers)[0]]); - } - // Only show the layers switch selector control if we have more than one baselayer + overlay - isLayersControlVisible = updateLayersControl(map, mapId, isLayersControlVisible, newBaseLayers, layers.overlays, leafletLayers); - }, true); - // Watch for the overlay layers - leafletScope.$watch('layers.overlays', function (newOverlayLayers) { - // Delete layers from the array - for (var name in leafletLayers.overlays) { - if (!isDefined(newOverlayLayers[name])) { - // Remove from the map if it's on it - if (map.hasLayer(leafletLayers.overlays[name])) { - map.removeLayer(leafletLayers.overlays[name]); - } - // TODO: Depending on the layer type we will have to delete what's included on it - delete leafletLayers.overlays[name]; + + if (map.hasLayer(ly)) { + if (ly.setOpacity) { + ly.setOpacity(op / 100); } - } - // add new overlays - for (var newName in newOverlayLayers) { - if (!isDefined(leafletLayers.overlays[newName])) { - var testOverlayLayer = createLayer(newOverlayLayers[newName]); - if (isDefined(testOverlayLayer)) { - leafletLayers.overlays[newName] = testOverlayLayer; - if (newOverlayLayers[newName].visible === true) { - map.addLayer(leafletLayers.overlays[newName]); + + if (ly.getLayers && ly.eachLayer) { + ly.eachLayer(function(lay) { + if (lay.setOpacity) { + lay.setOpacity(op / 100); } - } + }); } - // check for the .visible property to hide/show overLayers - if (newOverlayLayers[newName].visible && !map.hasLayer(leafletLayers.overlays[newName])) { - map.addLayer(leafletLayers.overlays[newName]); - } else if (newOverlayLayers[newName].visible === false && map.hasLayer(leafletLayers.overlays[newName])) { - map.removeLayer(leafletLayers.overlays[newName]); - } - } - // Only add the layers switch selector control if we have more than one baselayer + overlay - isLayersControlVisible = updateLayersControl(map, mapId, isLayersControlVisible, layers.baselayers, newOverlayLayers, leafletLayers); - }, true); - }); - } - }; - } - ]); - angular.module('leaflet-directive').directive('bounds', [ - '$log', - '$timeout', - 'leafletHelpers', - 'leafletBoundsHelpers', - function ($log, $timeout, leafletHelpers, leafletBoundsHelpers) { - return { - restrict: 'A', - scope: false, - replace: false, - require: [ - 'leaflet', - 'center' - ], - link: function (scope, element, attrs, controller) { - var isDefined = leafletHelpers.isDefined, createLeafletBounds = leafletBoundsHelpers.createLeafletBounds, leafletScope = controller[0].getLeafletScope(), mapController = controller[0]; - var emptyBounds = function (bounds) { - if (bounds._southWest.lat === 0 && bounds._southWest.lng === 0 && bounds._northEast.lat === 0 && bounds._northEast.lng === 0) { - return true; - } - return false; - }; - mapController.getMap().then(function (map) { - leafletScope.$on('boundsChanged', function (event) { - var scope = event.currentScope; - var bounds = map.getBounds(); - //$log.debug('updated map bounds...', bounds); - if (emptyBounds(bounds)) { - return; - } - var newScopeBounds = { - northEast: { - lat: bounds._northEast.lat, - lng: bounds._northEast.lng - }, - southWest: { - lat: bounds._southWest.lat, - lng: bounds._southWest.lng - } - }; - if (!angular.equals(scope.bounds, newScopeBounds)) { - //$log.debug('Need to update scope bounds.'); - scope.bounds = newScopeBounds; } }); - leafletScope.$watch('bounds', function (bounds) { - //$log.debug('updated bounds...', bounds); - if (!isDefined(bounds)) { - $log.error('[AngularJS - Leaflet] Invalid bounds'); - return; - } - var leafletBounds = createLeafletBounds(bounds); - if (leafletBounds && !map.getBounds().equals(leafletBounds)) { - //$log.debug('Need to update map bounds.'); - map.fitBounds(leafletBounds); - } - }, true); }); + }, + + changeGroupVisibility: function(groupName) { + if (!isDefined($scope.groupProperties[groupName])) { + return; + } + + var visible = $scope.groupProperties[groupName].visible; + for (var k in $scope.layers.overlays) { + var layer = $scope.layers.overlays[k]; + if (layer.group === groupName) { + layer.visible = visible; + } + } + }, + }); + + var div = $element.get(0); + if (!L.Browser.touch) { + L.DomEvent.disableClickPropagation(div); + L.DomEvent.on(div, 'mousewheel', L.DomEvent.stopPropagation); + } else { + L.DomEvent.on(div, 'click', L.DomEvent.stopPropagation); + } + }], + + template: + '
        ' + + '

        {{ title }}

        ' + + '
        ' + + '
        {{ baseTitle }}
        ' + + '
        ' + + '' + + '
        ' + + '
        ' + + '
        ' + + '
        {{ overlaysTitle }}
        ' + + '
        ' + + '
        ' + + '' + + '' + + '
        ' + + ' ' + + ' ' + + ' ' + + '' + + '
        ' + + '
        ' + + '
        ' + + '' + + '' + + '' + + '
        Range is not supported in this browser
        ' + + '
        ' + + '
        ' + + '
        ' + + '
        ' + + '
        ', + link: function(scope, element, attrs, controller) { + var isDefined = leafletHelpers.isDefined; + var leafletScope = controller.getLeafletScope(); + var layers = leafletScope.layers; + + scope.$watch('icons', function() { + var defaultIcons = { + uncheck: 'fa fa-square-o', + check: 'fa fa-check-square-o', + radio: 'fa fa-dot-circle-o', + unradio: 'fa fa-circle-o', + up: 'fa fa-angle-up', + down: 'fa fa-angle-down', + open: 'fa fa-angle-double-down', + close: 'fa fa-angle-double-up', + toggleLegend: 'fa fa-pencil-square-o', + }; + if (isDefined(scope.icons)) { + angular.extend(defaultIcons, scope.icons); + angular.extend(scope.icons, defaultIcons); + } else { + scope.icons = defaultIcons; + } + }); + + // Setting layer stack order. + attrs.order = (isDefined(attrs.order) && (attrs.order === 'normal' || attrs.order === 'reverse')) ? attrs.order : 'normal'; + scope.order = attrs.order === 'normal'; + scope.orderNumber = attrs.order === 'normal' ? -1 : 1; + + scope.layers = layers; + controller.getMap().then(function(map) { + leafletScope.$watch('layers.baselayers', function(newBaseLayers) { + var baselayersArray = {}; + leafletData.getLayers().then(function(leafletLayers) { + var key; + for (key in newBaseLayers) { + var layer = newBaseLayers[key]; + layer.icon = scope.icons[map.hasLayer(leafletLayers.baselayers[key]) ? 'radio' : 'unradio']; + baselayersArray[key] = layer; + } + + scope.baselayersArray = baselayersArray; + }); + }); + + leafletScope.$watch('layers.overlays', function(newOverlayLayers) { + var overlaysArray = []; + var groupVisibleCount = {}; + leafletData.getLayers().then(function(leafletLayers) { + var key; + for (key in newOverlayLayers) { + var layer = newOverlayLayers[key]; + layer.icon = scope.icons[(layer.visible ? 'check' : 'uncheck')]; + overlaysArray.push(layer); + if (!isDefined(scope.layerProperties[layer.name])) { + scope.layerProperties[layer.name] = { + opacity: isDefined(layer.layerOptions.opacity) ? layer.layerOptions.opacity * 100 : 100, + opacityControl: false, + showLegend: true, + }; + } + + if (isDefined(layer.group)) { + if (!isDefined(scope.groupProperties[layer.group])) { + scope.groupProperties[layer.group] = { + visible: false, + }; + } + + groupVisibleCount[layer.group] = isDefined(groupVisibleCount[layer.group]) ? groupVisibleCount[layer.group] : { + count: 0, + visibles: 0, + }; + groupVisibleCount[layer.group].count++; + if (layer.visible) { + groupVisibleCount[layer.group].visibles++; + } + } + + if (isDefined(layer.index) && leafletLayers.overlays[key].setZIndex) { + leafletLayers.overlays[key].setZIndex(newOverlayLayers[key].index); + } + } + + for (key in groupVisibleCount) { + scope.groupProperties[key].visible = groupVisibleCount[key].visibles === groupVisibleCount[key].count; + } + + scope.overlaysArray = overlaysArray; + }); + }, true); + }); + }, + }; +}]); + +angular.module('leaflet-directive').directive('layers', ["$log", "$q", "leafletData", "leafletHelpers", "leafletLayerHelpers", "leafletControlHelpers", function($log, $q, leafletData, leafletHelpers, leafletLayerHelpers, leafletControlHelpers) { + + return { + restrict: 'A', + scope: false, + replace: false, + require: 'leaflet', + controller: ["$scope", function($scope) { + $scope._leafletLayers = $q.defer(); + this.getLayers = function() { + return $scope._leafletLayers.promise; + }; + }], + + link: function(scope, element, attrs, controller) { + var isDefined = leafletHelpers.isDefined; + var leafletLayers = {}; + var leafletScope = controller.getLeafletScope(); + var layers = leafletScope.layers; + var createLayer = leafletLayerHelpers.createLayer; + var safeAddLayer = leafletLayerHelpers.safeAddLayer; + var safeRemoveLayer = leafletLayerHelpers.safeRemoveLayer; + var updateLayersControl = leafletControlHelpers.updateLayersControl; + var isLayersControlVisible = false; + + controller.getMap().then(function(map) { + + // We have baselayers to add to the map + scope._leafletLayers.resolve(leafletLayers); + leafletData.setLayers(leafletLayers, attrs.id); + + leafletLayers.baselayers = {}; + leafletLayers.overlays = {}; + + var mapId = attrs.id; + + // Setup all baselayers definitions + var oneVisibleLayer = false; + for (var layerName in layers.baselayers) { + var newBaseLayer = createLayer(layers.baselayers[layerName]); + if (!isDefined(newBaseLayer)) { + delete layers.baselayers[layerName]; + continue; + } + + leafletLayers.baselayers[layerName] = newBaseLayer; + + // Only add the visible layer to the map, layer control manages the addition to the map + // of layers in its control + if (layers.baselayers[layerName].top === true) { + safeAddLayer(map, leafletLayers.baselayers[layerName]); + oneVisibleLayer = true; + } + } + + // If there is no visible layer add first to the map + if (!oneVisibleLayer && Object.keys(leafletLayers.baselayers).length > 0) { + safeAddLayer(map, leafletLayers.baselayers[Object.keys(layers.baselayers)[0]]); + } + + // Setup the Overlays + for (layerName in layers.overlays) { + //if (layers.overlays[layerName].type === 'cartodb') { + // + //} + + var newOverlayLayer = createLayer(layers.overlays[layerName]); + if (!isDefined(newOverlayLayer)) { + delete layers.overlays[layerName]; + continue; + } + + leafletLayers.overlays[layerName] = newOverlayLayer; + + // Only add the visible overlays to the map + if (layers.overlays[layerName].visible === true) { + safeAddLayer(map, leafletLayers.overlays[layerName]); + } + } + + // Watch for the base layers + leafletScope.$watch('layers.baselayers', function(newBaseLayers, oldBaseLayers) { + if (angular.equals(newBaseLayers, oldBaseLayers)) { + isLayersControlVisible = updateLayersControl(map, mapId, isLayersControlVisible, newBaseLayers, layers.overlays, leafletLayers); + return true; + } + + // Delete layers from the array + for (var name in leafletLayers.baselayers) { + if (!isDefined(newBaseLayers[name]) || newBaseLayers[name].doRefresh) { + // Remove from the map if it's on it + if (map.hasLayer(leafletLayers.baselayers[name])) { + map.removeLayer(leafletLayers.baselayers[name]); + } + + delete leafletLayers.baselayers[name]; + + if (newBaseLayers[name] && newBaseLayers[name].doRefresh) { + newBaseLayers[name].doRefresh = false; + } + } + } + + // add new layers + for (var newName in newBaseLayers) { + if (!isDefined(leafletLayers.baselayers[newName])) { + var testBaseLayer = createLayer(newBaseLayers[newName]); + if (isDefined(testBaseLayer)) { + leafletLayers.baselayers[newName] = testBaseLayer; + + // Only add the visible layer to the map + if (newBaseLayers[newName].top === true) { + safeAddLayer(map, leafletLayers.baselayers[newName]); + } + } + } else { + if (newBaseLayers[newName].top === true && !map.hasLayer(leafletLayers.baselayers[newName])) { + safeAddLayer(map, leafletLayers.baselayers[newName]); + } else if (newBaseLayers[newName].top === false && map.hasLayer(leafletLayers.baselayers[newName])) { + map.removeLayer(leafletLayers.baselayers[newName]); + } + } + } + + //we have layers, so we need to make, at least, one active + var found = false; + + // search for an active layer + for (var key in leafletLayers.baselayers) { + if (map.hasLayer(leafletLayers.baselayers[key])) { + found = true; + break; + } + } + + // If there is no active layer make one active + if (!found && Object.keys(leafletLayers.baselayers).length > 0) { + safeAddLayer(map, leafletLayers.baselayers[Object.keys(leafletLayers.baselayers)[0]]); + } + + // Only show the layers switch selector control if we have more than one baselayer + overlay + isLayersControlVisible = updateLayersControl(map, mapId, isLayersControlVisible, newBaseLayers, layers.overlays, leafletLayers); + }, true); + + // Watch for the overlay layers + leafletScope.$watch('layers.overlays', function(newOverlayLayers, oldOverlayLayers) { + if (angular.equals(newOverlayLayers, oldOverlayLayers)) { + isLayersControlVisible = updateLayersControl(map, mapId, isLayersControlVisible, layers.baselayers, newOverlayLayers, leafletLayers); + return true; + } + + // Delete layers from the array + for (var name in leafletLayers.overlays) { + if (!isDefined(newOverlayLayers[name]) || newOverlayLayers[name].doRefresh) { + // Remove from the map if it's on it + if (map.hasLayer(leafletLayers.overlays[name])) { + // Safe remove when ArcGIS layers is loading. + var options = isDefined(newOverlayLayers[name]) ? + newOverlayLayers[name].layerOptions : null; + safeRemoveLayer(map, leafletLayers.overlays[name], options); + } + + // TODO: Depending on the layer type we will have to delete what's included on it + delete leafletLayers.overlays[name]; + + if (newOverlayLayers[name] && newOverlayLayers[name].doRefresh) { + newOverlayLayers[name].doRefresh = false; + } + } + } + + // add new overlays + for (var newName in newOverlayLayers) { + if (!isDefined(leafletLayers.overlays[newName])) { + var testOverlayLayer = createLayer(newOverlayLayers[newName]); + if (!isDefined(testOverlayLayer)) { + // If the layer creation fails, continue to the next overlay + continue; + } + + leafletLayers.overlays[newName] = testOverlayLayer; + if (newOverlayLayers[newName].visible === true) { + safeAddLayer(map, leafletLayers.overlays[newName]); + } + } else { + // check for the .visible property to hide/show overLayers + if (newOverlayLayers[newName].visible && !map.hasLayer(leafletLayers.overlays[newName])) { + safeAddLayer(map, leafletLayers.overlays[newName]); + } else if (newOverlayLayers[newName].visible === false && map.hasLayer(leafletLayers.overlays[newName])) { + // Safe remove when ArcGIS layers is loading. + safeRemoveLayer(map, leafletLayers.overlays[newName], newOverlayLayers[newName].layerOptions); + } + } + + //refresh heatmap data if present + if (newOverlayLayers[newName].visible && map._loaded && newOverlayLayers[newName].data && newOverlayLayers[newName].type === 'heatmap') { + leafletLayers.overlays[newName].setData(newOverlayLayers[newName].data); + leafletLayers.overlays[newName].update(); + } + } + + // Only add the layers switch selector control if we have more than one baselayer + overlay + isLayersControlVisible = updateLayersControl(map, mapId, isLayersControlVisible, layers.baselayers, newOverlayLayers, leafletLayers); + }, true); + }); + }, + }; +}]); + +angular.module('leaflet-directive').directive('legend', ["$log", "$http", "leafletHelpers", "leafletLegendHelpers", function($log, $http, leafletHelpers, leafletLegendHelpers) { + + return { + restrict: 'A', + scope: false, + replace: false, + require: 'leaflet', + + link: function(scope, element, attrs, controller) { + + var isArray = leafletHelpers.isArray; + var isDefined = leafletHelpers.isDefined; + var isFunction = leafletHelpers.isFunction; + var leafletScope = controller.getLeafletScope(); + var legend = leafletScope.legend; + + var legendClass; + var position; + var leafletLegend; + var type; + + leafletScope.$watch('legend', function(newLegend) { + + if (isDefined(newLegend)) { + + legendClass = newLegend.legendClass ? newLegend.legendClass : 'legend'; + + position = newLegend.position || 'bottomright'; + + // default to arcgis + type = newLegend.type || 'arcgis'; + } + + }, true); + + controller.getMap().then(function(map) { + + leafletScope.$watch('legend', function(newLegend) { + + if (!isDefined(newLegend)) { + + if (isDefined(leafletLegend)) { + leafletLegend.removeFrom(map); + leafletLegend = null; + } + + return; + } + + if (!isDefined(newLegend.url) && (type === 'arcgis') && (!isArray(newLegend.colors) || !isArray(newLegend.labels) || newLegend.colors.length !== newLegend.labels.length)) { + + $log.warn('[AngularJS - Leaflet] legend.colors and legend.labels must be set.'); + + return; + } + + if (isDefined(newLegend.url)) { + + $log.info('[AngularJS - Leaflet] loading legend service.'); + + return; + } + + if (isDefined(leafletLegend)) { + leafletLegend.removeFrom(map); + leafletLegend = null; + } + + leafletLegend = L.control({ + position: position, + }); + if (type === 'arcgis') { + leafletLegend.onAdd = leafletLegendHelpers.getOnAddArrayLegend(newLegend, legendClass); + } + + leafletLegend.addTo(map); + + }); + + leafletScope.$watch('legend.url', function(newURL) { + + if (!isDefined(newURL)) { + return; + } + + $http.get(newURL) + .success(function(legendData) { + + if (isDefined(leafletLegend)) { + + leafletLegendHelpers.updateLegend(leafletLegend.getContainer(), legendData, type, newURL); + + } else { + + leafletLegend = L.control({ + position: position, + }); + leafletLegend.onAdd = leafletLegendHelpers.getOnAddLegend(legendData, legendClass, type, newURL); + leafletLegend.addTo(map); + } + + if (isDefined(legend.loadedData) && isFunction(legend.loadedData)) { + legend.loadedData(); + } + }) + .error(function() { + $log.warn('[AngularJS - Leaflet] legend.url not loaded.'); + }); + }); + + }); + }, + }; +}]); + +angular.module('leaflet-directive').directive('markers', + ["$log", "$rootScope", "$q", "leafletData", "leafletHelpers", "leafletMapDefaults", "leafletMarkersHelpers", "leafletMarkerEvents", "leafletIterators", "leafletWatchHelpers", "leafletDirectiveControlsHelpers", function($log, $rootScope, $q, leafletData, leafletHelpers, leafletMapDefaults, + leafletMarkersHelpers, leafletMarkerEvents, leafletIterators, leafletWatchHelpers, + leafletDirectiveControlsHelpers) { + //less terse vars to helpers + var isDefined = leafletHelpers.isDefined; + var errorHeader = leafletHelpers.errorHeader; + var Helpers = leafletHelpers; + var isString = leafletHelpers.isString; + var addMarkerWatcher = leafletMarkersHelpers.addMarkerWatcher; + var updateMarker = leafletMarkersHelpers.updateMarker; + var listenMarkerEvents = leafletMarkersHelpers.listenMarkerEvents; + var addMarkerToGroup = leafletMarkersHelpers.addMarkerToGroup; + var createMarker = leafletMarkersHelpers.createMarker; + var deleteMarker = leafletMarkersHelpers.deleteMarker; + var $it = leafletIterators; + var _markersWatchOptions = leafletHelpers.watchOptions; + var maybeWatch = leafletWatchHelpers.maybeWatch; + var extendDirectiveControls = leafletDirectiveControlsHelpers.extend; + + var _getLMarker = function(leafletMarkers, name, maybeLayerName) { + if (!Object.keys(leafletMarkers).length) return; + if (maybeLayerName && isString(maybeLayerName)) { + if (!leafletMarkers[maybeLayerName] || !Object.keys(leafletMarkers[maybeLayerName]).length) + return; + return leafletMarkers[maybeLayerName][name]; + } + + return leafletMarkers[name]; + }; + + var _setLMarker = function(lObject, leafletMarkers, name, maybeLayerName) { + if (maybeLayerName && isString(maybeLayerName)) { + if (!isDefined(leafletMarkers[maybeLayerName])) + leafletMarkers[maybeLayerName] = {}; + leafletMarkers[maybeLayerName][name] = lObject; + } else + leafletMarkers[name] = lObject; + return lObject; + }; + + var _maybeAddMarkerToLayer = function(layerName, layers, model, marker, doIndividualWatch, map) { + + if (!isString(layerName)) { + $log.error(errorHeader + ' A layername must be a string'); + return false; + } + + if (!isDefined(layers)) { + $log.error(errorHeader + ' You must add layers to the directive if the markers are going to use this functionality.'); + return false; + } + + if (!isDefined(layers.overlays) || !isDefined(layers.overlays[layerName])) { + $log.error(errorHeader + ' A marker can only be added to a layer of type "group"'); + return false; + } + + var layerGroup = layers.overlays[layerName]; + if (!(layerGroup instanceof L.LayerGroup || layerGroup instanceof L.FeatureGroup)) { + $log.error(errorHeader + ' Adding a marker to an overlay needs a overlay of the type "group" or "featureGroup"'); + return false; + } + + // The marker goes to a correct layer group, so first of all we add it + layerGroup.addLayer(marker); + + // The marker is automatically added to the map depending on the visibility + // of the layer, so we only have to open the popup if the marker is in the map + if (!doIndividualWatch && map.hasLayer(marker) && model.focus === true) { + marker.openPopup(); + } + + return true; + }; + + //TODO: move to leafletMarkersHelpers??? or make a new class/function file (leafletMarkersHelpers is large already) + var _addMarkers = function(mapId, markersToRender, oldModels, map, layers, leafletMarkers, leafletScope, + watchOptions, maybeLayerName, skips) { + for (var newName in markersToRender) { + if (skips[newName]) + continue; + + if (newName.search('-') !== -1) { + $log.error('The marker can\'t use a "-" on his key name: "' + newName + '".'); + continue; + } + + var model = Helpers.copy(markersToRender[newName]); + var pathToMarker = Helpers.getObjectDotPath(maybeLayerName ? [maybeLayerName, newName] : [newName]); + var maybeLMarker = _getLMarker(leafletMarkers, newName, maybeLayerName); + if (!isDefined(maybeLMarker)) { + //(nmccready) very important to not have model changes when lObject is changed + //this might be desirable in some cases but it causes two-way binding to lObject which is not ideal + //if it is left as the reference then all changes from oldModel vs newModel are ignored + //see _destroy (where modelDiff becomes meaningless if we do not copy here) + var marker = createMarker(model); + var layerName = (model ? model.layer : undefined) || maybeLayerName; //original way takes pref + if (!isDefined(marker)) { + $log.error(errorHeader + ' Received invalid data on the marker ' + newName + '.'); + continue; + } + + _setLMarker(marker, leafletMarkers, newName, maybeLayerName); + + // Bind message + if (isDefined(model.message)) { + marker.bindPopup(model.message, model.popupOptions); + } + + // Add the marker to a cluster group if needed + if (isDefined(model.group)) { + var groupOptions = isDefined(model.groupOption) ? model.groupOption : null; + addMarkerToGroup(marker, model.group, groupOptions, map); + } + + // Show label if defined + if (Helpers.LabelPlugin.isLoaded() && isDefined(model.label) && isDefined(model.label.message)) { + marker.bindLabel(model.label.message, model.label.options); + } + + // Check if the marker should be added to a layer + if (isDefined(model) && (isDefined(model.layer) || isDefined(maybeLayerName))) { + + var pass = _maybeAddMarkerToLayer(layerName, layers, model, marker, + watchOptions.individual.doWatch, map); + if (!pass) + continue; //something went wrong move on in the loop + } else if (!isDefined(model.group)) { + // We do not have a layer attr, so the marker goes to the map layer + map.addLayer(marker); + if (!watchOptions.individual.doWatch && model.focus === true) { + marker.openPopup(); + } + } + + if (watchOptions.individual.doWatch) { + addMarkerWatcher(marker, pathToMarker, leafletScope, layers, map, + watchOptions.individual.isDeep); + } + + listenMarkerEvents(marker, model, leafletScope, watchOptions.individual.doWatch, map); + leafletMarkerEvents.bindEvents(mapId, marker, pathToMarker, model, leafletScope, layerName); + } else { + var oldModel = isDefined(oldModel) ? oldModels[newName] : undefined; + updateMarker(model, oldModel, maybeLMarker, pathToMarker, leafletScope, layers, map); + } } }; - } - ]); - angular.module('leaflet-directive').directive('markers', [ - '$log', - '$rootScope', - '$q', - 'leafletData', - 'leafletHelpers', - 'leafletMapDefaults', - 'leafletMarkersHelpers', - 'leafletEvents', - function ($log, $rootScope, $q, leafletData, leafletHelpers, leafletMapDefaults, leafletMarkersHelpers, leafletEvents) { + + var _seeWhatWeAlreadyHave = function(markerModels, oldMarkerModels, lMarkers, isEqual, cb) { + var hasLogged = false; + var equals = false; + var oldMarker; + var newMarker; + + var doCheckOldModel = isDefined(oldMarkerModels); + for (var name in lMarkers) { + if (!hasLogged) { + $log.debug(errorHeader + '[markers] destroy: '); + hasLogged = true; + } + + if (doCheckOldModel) { + //might want to make the option (in watch options) to disable deep checking + //ie the options to only check !== (reference check) instead of angular.equals (slow) + newMarker = markerModels[name]; + oldMarker = oldMarkerModels[name]; + equals = angular.equals(newMarker, oldMarker) && isEqual; + } + + if (!isDefined(markerModels) || + !Object.keys(markerModels).length || + !isDefined(markerModels[name]) || + !Object.keys(markerModels[name]).length || + equals) { + if (cb && Helpers.isFunction(cb)) + cb(newMarker, oldMarker, name); + } + } + }; + + var _destroy = function(markerModels, oldMarkerModels, lMarkers, map, layers) { + _seeWhatWeAlreadyHave(markerModels, oldMarkerModels, lMarkers, false, + function(newMarker, oldMarker, lMarkerName) { + $log.debug(errorHeader + '[marker] is deleting marker: ' + lMarkerName); + deleteMarker(lMarkers[lMarkerName], map, layers); + delete lMarkers[lMarkerName]; + }); + }; + + var _getNewModelsToSkipp = function(newModels, oldModels, lMarkers) { + var skips = {}; + _seeWhatWeAlreadyHave(newModels, oldModels, lMarkers, true, + function(newMarker, oldMarker, lMarkerName) { + $log.debug(errorHeader + '[marker] is already rendered, marker: ' + lMarkerName); + skips[lMarkerName] = newMarker; + }); + + return skips; + }; + return { restrict: 'A', scope: false, replace: false, - require: [ - 'leaflet', - '?layers' - ], - link: function (scope, element, attrs, controller) { - var mapController = controller[0], Helpers = leafletHelpers, isDefined = leafletHelpers.isDefined, isString = leafletHelpers.isString, leafletScope = mapController.getLeafletScope(), markers = leafletScope.markers, deleteMarker = leafletMarkersHelpers.deleteMarker, addMarkerWatcher = leafletMarkersHelpers.addMarkerWatcher, listenMarkerEvents = leafletMarkersHelpers.listenMarkerEvents, addMarkerToGroup = leafletMarkersHelpers.addMarkerToGroup, bindMarkerEvents = leafletEvents.bindMarkerEvents, createMarker = leafletMarkersHelpers.createMarker; - mapController.getMap().then(function (map) { - var leafletMarkers = {}, getLayers; + require: ['leaflet', '?layers'], + + link: function(scope, element, attrs, controller) { + var mapController = controller[0]; + var leafletScope = mapController.getLeafletScope(); + + mapController.getMap().then(function(map) { + var leafletMarkers = {}; + var getLayers; + // If the layers attribute is used, we must wait until the layers are created if (isDefined(controller[1])) { getLayers = controller[1].getLayers; } else { - getLayers = function () { + getLayers = function() { var deferred = $q.defer(); deferred.resolve(); return deferred.promise; }; } - if (!isDefined(markers)) { - return; - } - getLayers().then(function (layers) { + + var watchOptions = leafletScope.markersWatchOptions || _markersWatchOptions; + + // backwards compat + if (isDefined(attrs.watchMarkers)) + watchOptions.doWatch = watchOptions.individual.doWatch = + (!isDefined(attrs.watchMarkers) || Helpers.isTruthy(attrs.watchMarkers)); + + var isNested = (isDefined(attrs.markersNested) && Helpers.isTruthy(attrs.markersNested)); + + getLayers().then(function(layers) { + var _clean = function(models, oldModels) { + if (isNested) { + $it.each(models, function(markerToMaybeDel, layerName) { + var oldModel = isDefined(oldModel) ? oldModels[layerName] : undefined; + _destroy(markerToMaybeDel, oldModel, leafletMarkers[layerName], map, layers); + }); + + return; + } + + _destroy(models, oldModels, leafletMarkers, map, layers); + }; + + var _create = function(models, oldModels) { + _clean(models, oldModels); + var skips = null; + if (isNested) { + $it.each(models, function(markersToAdd, layerName) { + var oldModel = isDefined(oldModel) ? oldModels[layerName] : undefined; + skips = _getNewModelsToSkipp(models[layerName], oldModel, leafletMarkers[layerName]); + _addMarkers(attrs.id, markersToAdd, oldModels, map, layers, leafletMarkers, leafletScope, + watchOptions, layerName, skips); + }); + + return; + } + + skips = _getNewModelsToSkipp(models, oldModels, leafletMarkers); + _addMarkers(attrs.id, models, oldModels, map, layers, leafletMarkers, leafletScope, + watchOptions, undefined, skips); + }; + + extendDirectiveControls(attrs.id, 'markers', _create, _clean); leafletData.setMarkers(leafletMarkers, attrs.id); - leafletScope.$watch('markers', function (newMarkers) { - // Delete markers from the array - for (var name in leafletMarkers) { - if (!isDefined(newMarkers) || !isDefined(newMarkers[name])) { - deleteMarker(leafletMarkers[name], map, layers); - delete leafletMarkers[name]; - } - } - // add new markers - for (var newName in newMarkers) { - if (newName.search('-') !== -1) { - $log.error('The marker can\'t use a "-" on his key name: "' + newName + '".'); - continue; - } - if (!isDefined(leafletMarkers[newName])) { - var markerData = newMarkers[newName]; - var marker = createMarker(markerData); - if (!isDefined(marker)) { - $log.error('[AngularJS - Leaflet] Received invalid data on the marker ' + newName + '.'); - continue; - } - leafletMarkers[newName] = marker; - // Bind message - if (isDefined(markerData.message)) { - marker.bindPopup(markerData.message, markerData.popupOptions); - } - // Add the marker to a cluster group if needed - if (isDefined(markerData.group)) { - addMarkerToGroup(marker, markerData.group, map); - } - // Show label if defined - if (Helpers.LabelPlugin.isLoaded() && isDefined(markerData.label) && isDefined(markerData.label.message)) { - marker.bindLabel(markerData.label.message, markerData.label.options); - } - // Check if the marker should be added to a layer - if (isDefined(markerData) && isDefined(markerData.layer)) { - if (!isString(markerData.layer)) { - $log.error('[AngularJS - Leaflet] A layername must be a string'); - continue; - } - if (!isDefined(layers)) { - $log.error('[AngularJS - Leaflet] You must add layers to the directive if the markers are going to use this functionality.'); - continue; - } - if (!isDefined(layers.overlays) || !isDefined(layers.overlays[markerData.layer])) { - $log.error('[AngularJS - Leaflet] A marker can only be added to a layer of type "group"'); - continue; - } - var layerGroup = layers.overlays[markerData.layer]; - if (!(layerGroup instanceof L.LayerGroup)) { - $log.error('[AngularJS - Leaflet] Adding a marker to an overlay needs a overlay of the type "group"'); - continue; - } - // The marker goes to a correct layer group, so first of all we add it - layerGroup.addLayer(marker); - // The marker is automatically added to the map depending on the visibility - // of the layer, so we only have to open the popup if the marker is in the map - if (map.hasLayer(marker) && markerData.focus === true) { - marker.openPopup(); - } // Add the marker to the map if it hasn't been added to a layer or to a group - } else if (!isDefined(markerData.group)) { - // We do not have a layer attr, so the marker goes to the map layer - map.addLayer(marker); - if (markerData.focus === true) { - marker.openPopup(); - } - if (Helpers.LabelPlugin.isLoaded() && isDefined(markerData.label) && isDefined(markerData.label.options) && markerData.label.options.noHide === true) { - marker.showLabel(); - } - } - // Should we watch for every specific marker on the map? - var shouldWatch = !isDefined(attrs.watchMarkers) || attrs.watchMarkers === 'true'; - if (shouldWatch) { - addMarkerWatcher(marker, newName, leafletScope, layers, map); - listenMarkerEvents(marker, markerData, leafletScope); - } - bindMarkerEvents(marker, newName, markerData, leafletScope); - } - } - }, true); + + maybeWatch(leafletScope, 'markers', watchOptions, function(newMarkers, oldMarkers) { + _create(newMarkers, oldMarkers); + }); }); }); - } + }, }; - } - ]); - angular.module('leaflet-directive').directive('paths', [ - '$log', - 'leafletData', - 'leafletMapDefaults', - 'leafletHelpers', - 'leafletPathsHelpers', - 'leafletEvents', - function ($log, leafletData, leafletMapDefaults, leafletHelpers, leafletPathsHelpers, leafletEvents) { - return { - restrict: 'A', - scope: false, - replace: false, - require: 'leaflet', - link: function (scope, element, attrs, controller) { - var isDefined = leafletHelpers.isDefined, leafletScope = controller.getLeafletScope(), paths = leafletScope.paths, createPath = leafletPathsHelpers.createPath, bindPathEvents = leafletEvents.bindPathEvents, setPathOptions = leafletPathsHelpers.setPathOptions; - controller.getMap().then(function (map) { - var defaults = leafletMapDefaults.getDefaults(attrs.id); - if (!isDefined(paths)) { - return; - } - var leafletPaths = {}; - leafletData.setPaths(leafletPaths, attrs.id); - // Function for listening every single path once created - var watchPathFn = function (leafletPath, name) { - var clearWatch = leafletScope.$watch('paths.' + name, function (pathData) { - if (!isDefined(pathData)) { - map.removeLayer(leafletPath); - clearWatch(); - return; - } - setPathOptions(leafletPath, pathData.type, pathData); - }, true); - }; - leafletScope.$watch('paths', function (newPaths) { - // Create the new paths - for (var newName in newPaths) { - if (newName.search('-') !== -1) { - $log.error('[AngularJS - Leaflet] The path name "' + newName + '" is not valid. It must not include "-" and a number.'); - continue; - } - if (!isDefined(leafletPaths[newName])) { - var pathData = newPaths[newName]; - var newPath = createPath(newName, newPaths[newName], defaults); - // bind popup if defined - if (isDefined(newPath) && isDefined(pathData.message)) { - newPath.bindPopup(pathData.message); - } - // Show label if defined - if (leafletHelpers.LabelPlugin.isLoaded() && isDefined(pathData.label) && isDefined(pathData.label.message)) { - newPath.bindLabel(pathData.label.message, pathData.label.options); - } - // Listen for changes on the new path - if (isDefined(newPath)) { - leafletPaths[newName] = newPath; - map.addLayer(newPath); - watchPathFn(newPath, newName); - } - bindPathEvents(newPath, newName, pathData, leafletScope); - } - } - // Delete paths (by name) from the array - for (var name in leafletPaths) { - if (!isDefined(newPaths[name])) { - delete leafletPaths[name]; - } - } - }, true); - }); - } - }; - } - ]); - angular.module('leaflet-directive').directive('controls', [ - '$log', - 'leafletHelpers', - function ($log, leafletHelpers) { - return { - restrict: 'A', - scope: false, - replace: false, - require: '?^leaflet', - link: function (scope, element, attrs, controller) { - if (!controller) { + }]); + +angular.module('leaflet-directive').directive('maxbounds', ["$log", "leafletMapDefaults", "leafletBoundsHelpers", "leafletHelpers", function($log, leafletMapDefaults, leafletBoundsHelpers, leafletHelpers) { + + return { + restrict: 'A', + scope: false, + replace: false, + require: 'leaflet', + + link: function(scope, element, attrs, controller) { + var leafletScope = controller.getLeafletScope(); + var isValidBounds = leafletBoundsHelpers.isValidBounds; + var isNumber = leafletHelpers.isNumber; + + controller.getMap().then(function(map) { + leafletScope.$watch('maxbounds', function(maxbounds) { + if (!isValidBounds(maxbounds)) { + // Unset any previous maxbounds + map.setMaxBounds(); return; } - var isDefined = leafletHelpers.isDefined, leafletScope = controller.getLeafletScope(), controls = leafletScope.controls; - controller.getMap().then(function (map) { - if (isDefined(L.Control.Draw) && isDefined(controls.draw)) { - var drawnItems = new L.FeatureGroup(); - map.addLayer(drawnItems); - var options = { edit: { featureGroup: drawnItems } }; - angular.extend(options, controls.draw.options); - var drawControl = new L.Control.Draw(options); - map.addControl(drawControl); - } - if (isDefined(controls.custom)) { - for (var i in controls.custom) { - map.addControl(controls.custom[i]); - } - } - }); - } - }; - } - ]); - angular.module('leaflet-directive').directive('eventBroadcast', [ - '$log', - '$rootScope', - 'leafletHelpers', - 'leafletEvents', - function ($log, $rootScope, leafletHelpers, leafletEvents) { - return { - restrict: 'A', - scope: false, - replace: false, - require: 'leaflet', - link: function (scope, element, attrs, controller) { - var isObject = leafletHelpers.isObject, leafletScope = controller.getLeafletScope(), eventBroadcast = leafletScope.eventBroadcast, availableMapEvents = leafletEvents.getAvailableMapEvents(), genDispatchMapEvent = leafletEvents.genDispatchMapEvent; - controller.getMap().then(function (map) { - var mapEvents = []; - var i; - var eventName; - var logic = 'broadcast'; - if (isObject(eventBroadcast)) { - // We have a possible valid object - if (eventBroadcast.map === undefined || eventBroadcast.map === null) { - // We do not have events enable/disable do we do nothing (all enabled by default) - mapEvents = availableMapEvents; - } else if (typeof eventBroadcast.map !== 'object') { - // Not a valid object - $log.warn('[AngularJS - Leaflet] event-broadcast.map must be an object check your model.'); - } else { - // We have a possible valid map object - // Event propadation logic - if (eventBroadcast.map.logic !== undefined && eventBroadcast.map.logic !== null) { - // We take care of possible propagation logic - if (eventBroadcast.map.logic !== 'emit' && eventBroadcast.map.logic !== 'broadcast') { - // This is an error - $log.warn('[AngularJS - Leaflet] Available event propagation logic are: \'emit\' or \'broadcast\'.'); - } else if (eventBroadcast.map.logic === 'emit') { - logic = 'emit'; - } - } - // Enable / Disable - var mapEventsEnable = false, mapEventsDisable = false; - if (eventBroadcast.map.enable !== undefined && eventBroadcast.map.enable !== null) { - if (typeof eventBroadcast.map.enable === 'object') { - mapEventsEnable = true; - } - } - if (eventBroadcast.map.disable !== undefined && eventBroadcast.map.disable !== null) { - if (typeof eventBroadcast.map.disable === 'object') { - mapEventsDisable = true; - } - } - if (mapEventsEnable && mapEventsDisable) { - // Both are active, this is an error - $log.warn('[AngularJS - Leaflet] can not enable and disable events at the time'); - } else if (!mapEventsEnable && !mapEventsDisable) { - // Both are inactive, this is an error - $log.warn('[AngularJS - Leaflet] must enable or disable events'); - } else { - // At this point the map object is OK, lets enable or disable events - if (mapEventsEnable) { - // Enable events - for (i = 0; i < eventBroadcast.map.enable.length; i++) { - eventName = eventBroadcast.map.enable[i]; - // Do we have already the event enabled? - if (mapEvents.indexOf(eventName) !== -1) { - // Repeated event, this is an error - $log.warn('[AngularJS - Leaflet] This event ' + eventName + ' is already enabled'); - } else { - // Does the event exists? - if (availableMapEvents.indexOf(eventName) === -1) { - // The event does not exists, this is an error - $log.warn('[AngularJS - Leaflet] This event ' + eventName + ' does not exist'); - } else { - // All ok enable the event - mapEvents.push(eventName); - } - } - } - } else { - // Disable events - mapEvents = availableMapEvents; - for (i = 0; i < eventBroadcast.map.disable.length; i++) { - eventName = eventBroadcast.map.disable[i]; - var index = mapEvents.indexOf(eventName); - if (index === -1) { - // The event does not exist - $log.warn('[AngularJS - Leaflet] This event ' + eventName + ' does not exist or has been already disabled'); - } else { - mapEvents.splice(index, 1); - } - } - } - } - } - for (i = 0; i < mapEvents.length; i++) { - eventName = mapEvents[i]; - map.on(eventName, genDispatchMapEvent(leafletScope, eventName, logic), { eventName: eventName }); - } - } else { - // Not a valid object - $log.warn('[AngularJS - Leaflet] event-broadcast must be an object, check your model.'); - } - }); - } - }; - } - ]); - angular.module('leaflet-directive').directive('maxbounds', [ - '$log', - 'leafletMapDefaults', - 'leafletBoundsHelpers', - function ($log, leafletMapDefaults, leafletBoundsHelpers) { - return { - restrict: 'A', - scope: false, - replace: false, - require: 'leaflet', - link: function (scope, element, attrs, controller) { - var leafletScope = controller.getLeafletScope(), isValidBounds = leafletBoundsHelpers.isValidBounds; - controller.getMap().then(function (map) { - leafletScope.$watch('maxbounds', function (maxbounds) { - if (!isValidBounds(maxbounds)) { - // Unset any previous maxbounds - map.setMaxBounds(); - return; - } - var bounds = [ - [ - maxbounds.southWest.lat, - maxbounds.southWest.lng - ], - [ - maxbounds.northEast.lat, - maxbounds.northEast.lng - ] - ]; - map.setMaxBounds(bounds); - map.fitBounds(bounds); - }); - }); - } - }; - } - ]); - angular.module('leaflet-directive').service('leafletData', [ - '$log', - '$q', - 'leafletHelpers', - function ($log, $q, leafletHelpers) { - var getDefer = leafletHelpers.getDefer, getUnresolvedDefer = leafletHelpers.getUnresolvedDefer, setResolvedDefer = leafletHelpers.setResolvedDefer; - var maps = {}; - var tiles = {}; - var layers = {}; - var paths = {}; - var markers = {}; - var geoJSON = {}; - this.setMap = function (leafletMap, scopeId) { - var defer = getUnresolvedDefer(maps, scopeId); - defer.resolve(leafletMap); - setResolvedDefer(maps, scopeId); - }; - this.getMap = function (scopeId) { - var defer = getDefer(maps, scopeId); - return defer.promise; - }; - this.unresolveMap = function (scopeId) { - var id = leafletHelpers.obtainEffectiveMapId(maps, scopeId); - maps[id] = undefined; - }; - this.getPaths = function (scopeId) { - var defer = getDefer(paths, scopeId); - return defer.promise; - }; - this.setPaths = function (leafletPaths, scopeId) { - var defer = getUnresolvedDefer(paths, scopeId); - defer.resolve(leafletPaths); - setResolvedDefer(paths, scopeId); - }; - this.getMarkers = function (scopeId) { - var defer = getDefer(markers, scopeId); - return defer.promise; - }; - this.setMarkers = function (leafletMarkers, scopeId) { - var defer = getUnresolvedDefer(markers, scopeId); - defer.resolve(leafletMarkers); - setResolvedDefer(markers, scopeId); - }; - this.getLayers = function (scopeId) { - var defer = getDefer(layers, scopeId); - return defer.promise; - }; - this.setLayers = function (leafletLayers, scopeId) { - var defer = getUnresolvedDefer(layers, scopeId); - defer.resolve(leafletLayers); - setResolvedDefer(layers, scopeId); - }; - this.setTiles = function (leafletTiles, scopeId) { - var defer = getUnresolvedDefer(tiles, scopeId); - defer.resolve(leafletTiles); - setResolvedDefer(tiles, scopeId); - }; - this.getTiles = function (scopeId) { - var defer = getDefer(tiles, scopeId); - return defer.promise; - }; - this.setGeoJSON = function (leafletGeoJSON, scopeId) { - var defer = getUnresolvedDefer(geoJSON, scopeId); - defer.resolve(leafletGeoJSON); - setResolvedDefer(geoJSON, scopeId); - }; - this.getGeoJSON = function (scopeId) { - var defer = getDefer(geoJSON, scopeId); - return defer.promise; - }; - } - ]); - angular.module('leaflet-directive').factory('leafletMapDefaults', [ - '$q', - 'leafletHelpers', - function ($q, leafletHelpers) { - function _getDefaults() { - return { - keyboard: true, - dragging: true, - worldCopyJump: false, - doubleClickZoom: true, - scrollWheelZoom: true, - zoomControl: true, - zoomsliderControl: false, - zoomControlPosition: 'topleft', - attributionControl: true, - controls: { - layers: { - visible: true, - position: 'topright', - collapsed: true - } - }, - crs: L.CRS.EPSG3857, - tileLayer: 'http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', - tileLayerOptions: { attribution: '© OpenStreetMap contributors' }, - path: { - weight: 10, - opacity: 1, - color: '#0000ff' - }, - center: { - lat: 0, - lng: 0, - zoom: 1 + + var leafletBounds = leafletBoundsHelpers.createLeafletBounds(maxbounds); + if (isNumber(maxbounds.pad)) { + leafletBounds = leafletBounds.pad(maxbounds.pad); } - }; - } - var isDefined = leafletHelpers.isDefined, obtainEffectiveMapId = leafletHelpers.obtainEffectiveMapId, defaults = {}; - // Get the _defaults dictionary, and override the properties defined by the user - return { - getDefaults: function (scopeId) { - var mapId = obtainEffectiveMapId(defaults, scopeId); - return defaults[mapId]; - }, - getMapCreationDefaults: function (scopeId) { - var mapId = obtainEffectiveMapId(defaults, scopeId); - var d = defaults[mapId]; - var mapDefaults = { - maxZoom: d.maxZoom, - keyboard: d.keyboard, - dragging: d.dragging, - zoomControl: d.zoomControl, - doubleClickZoom: d.doubleClickZoom, - scrollWheelZoom: d.scrollWheelZoom, - attributionControl: d.attributionControl, - worldCopyJump: d.worldCopyJump, - crs: d.crs - }; - if (isDefined(d.minZoom)) { - mapDefaults.minZoom = d.minZoom; + + map.setMaxBounds(leafletBounds); + if (!attrs.center && !attrs.lfCenter) { + map.fitBounds(leafletBounds); } - if (isDefined(d.zoomAnimation)) { - mapDefaults.zoomAnimation = d.zoomAnimation; - } - if (isDefined(d.fadeAnimation)) { - mapDefaults.fadeAnimation = d.fadeAnimation; - } - if (isDefined(d.markerZoomAnimation)) { - mapDefaults.markerZoomAnimation = d.markerZoomAnimation; - } - if (d.map) { - for (var option in d.map) { - mapDefaults[option] = d.map[option]; - } - } - return mapDefaults; - }, - setDefaults: function (userDefaults, scopeId) { - var newDefaults = _getDefaults(); - if (isDefined(userDefaults)) { - newDefaults.doubleClickZoom = isDefined(userDefaults.doubleClickZoom) ? userDefaults.doubleClickZoom : newDefaults.doubleClickZoom; - newDefaults.scrollWheelZoom = isDefined(userDefaults.scrollWheelZoom) ? userDefaults.scrollWheelZoom : newDefaults.doubleClickZoom; - newDefaults.zoomControl = isDefined(userDefaults.zoomControl) ? userDefaults.zoomControl : newDefaults.zoomControl; - newDefaults.zoomsliderControl = isDefined(userDefaults.zoomsliderControl) ? userDefaults.zoomsliderControl : newDefaults.zoomsliderControl; - newDefaults.attributionControl = isDefined(userDefaults.attributionControl) ? userDefaults.attributionControl : newDefaults.attributionControl; - newDefaults.tileLayer = isDefined(userDefaults.tileLayer) ? userDefaults.tileLayer : newDefaults.tileLayer; - newDefaults.zoomControlPosition = isDefined(userDefaults.zoomControlPosition) ? userDefaults.zoomControlPosition : newDefaults.zoomControlPosition; - newDefaults.keyboard = isDefined(userDefaults.keyboard) ? userDefaults.keyboard : newDefaults.keyboard; - newDefaults.dragging = isDefined(userDefaults.dragging) ? userDefaults.dragging : newDefaults.dragging; - if (isDefined(userDefaults.controls)) { - angular.extend(newDefaults.controls, userDefaults.controls); - } - if (isDefined(userDefaults.crs) && isDefined(L.CRS[userDefaults.crs])) { - newDefaults.crs = L.CRS[userDefaults.crs]; - } - if (isDefined(userDefaults.tileLayerOptions)) { - angular.copy(userDefaults.tileLayerOptions, newDefaults.tileLayerOptions); - } - if (isDefined(userDefaults.maxZoom)) { - newDefaults.maxZoom = userDefaults.maxZoom; - } - if (isDefined(userDefaults.minZoom)) { - newDefaults.minZoom = userDefaults.minZoom; - } - if (isDefined(userDefaults.zoomAnimation)) { - newDefaults.zoomAnimation = userDefaults.zoomAnimation; - } - if (isDefined(userDefaults.fadeAnimation)) { - newDefaults.fadeAnimation = userDefaults.fadeAnimation; - } - if (isDefined(userDefaults.markerZoomAnimation)) { - newDefaults.markerZoomAnimation = userDefaults.markerZoomAnimation; - } - if (isDefined(userDefaults.worldCopyJump)) { - newDefaults.worldCopyJump = userDefaults.worldCopyJump; - } - if (isDefined(userDefaults.map)) { - newDefaults.map = userDefaults.map; - } - } - var mapId = obtainEffectiveMapId(defaults, scopeId); - defaults[mapId] = newDefaults; - return newDefaults; - } - }; - } - ]); - angular.module('leaflet-directive').factory('leafletEvents', [ - '$rootScope', - '$q', - '$log', - 'leafletHelpers', - function ($rootScope, $q, $log, leafletHelpers) { - var safeApply = leafletHelpers.safeApply, isDefined = leafletHelpers.isDefined, isObject = leafletHelpers.isObject, Helpers = leafletHelpers; - var _getAvailableLabelEvents = function () { - return [ - 'click', - 'dblclick', - 'mousedown', - 'mouseover', - 'mouseout', - 'contextmenu' - ]; - }; - var genLabelEvents = function (leafletScope, logic, marker, name) { - var labelEvents = _getAvailableLabelEvents(); - var scopeWatchName = 'markers.' + name; - for (var i = 0; i < labelEvents.length; i++) { - var eventName = labelEvents[i]; - marker.label.on(eventName, genDispatchLabelEvent(leafletScope, eventName, logic, marker.label, scopeWatchName)); - } - }; - var genDispatchMarkerEvent = function (eventName, logic, leafletScope, marker, name, markerData) { - return function (e) { - var broadcastName = 'leafletDirectiveMarker.' + eventName; - // Broadcast old marker click name for backwards compatibility - if (eventName === 'click') { - safeApply(leafletScope, function () { - $rootScope.$broadcast('leafletDirectiveMarkersClick', name); - }); - } else if (eventName === 'dragend') { - safeApply(leafletScope, function () { - markerData.lat = marker.getLatLng().lat; - markerData.lng = marker.getLatLng().lng; - }); - if (markerData.message && markerData.focus === true) { - marker.openPopup(); - } - } - safeApply(leafletScope, function (scope) { - if (logic === 'emit') { - scope.$emit(broadcastName, { - markerName: name, - leafletEvent: e - }); - } else { - $rootScope.$broadcast(broadcastName, { - markerName: name, - leafletEvent: e - }); - } - }); - }; - }; - var genDispatchPathEvent = function (eventName, logic, leafletScope, marker, name) { - return function (e) { - var broadcastName = 'leafletDirectivePath.' + eventName; - safeApply(leafletScope, function (scope) { - if (logic === 'emit') { - scope.$emit(broadcastName, { - pathName: name, - leafletEvent: e - }); - } else { - $rootScope.$broadcast(broadcastName, { - pathName: name, - leafletEvent: e - }); - } - }); - }; - }; - var genDispatchLabelEvent = function (scope, eventName, logic, label, scope_watch_name) { - return function (e) { - // Put together broadcast name - var broadcastName = 'leafletDirectiveLabel.' + eventName; - var markerName = scope_watch_name.replace('markers.', ''); - // Safely broadcast the event - safeApply(scope, function (scope) { - if (logic === 'emit') { - scope.$emit(broadcastName, { - leafletEvent: e, - label: label, - markerName: markerName - }); - } else if (logic === 'broadcast') { - $rootScope.$broadcast(broadcastName, { - leafletEvent: e, - label: label, - markerName: markerName - }); - } - }); - }; - }; - var _getAvailableMarkerEvents = function () { - return [ - 'click', - 'dblclick', - 'mousedown', - 'mouseover', - 'mouseout', - 'contextmenu', - 'dragstart', - 'drag', - 'dragend', - 'move', - 'remove', - 'popupopen', - 'popupclose' - ]; - }; - var _getAvailablePathEvents = function () { - return [ - 'click', - 'dblclick', - 'mousedown', - 'mouseover', - 'mouseout', - 'contextmenu', - 'add', - 'remove', - 'popupopen', - 'popupclose' - ]; - }; - return { - getAvailableMapEvents: function () { - return [ - 'click', - 'dblclick', - 'mousedown', - 'mouseup', - 'mouseover', - 'mouseout', - 'mousemove', - 'contextmenu', - 'focus', - 'blur', - 'preclick', - 'load', - 'unload', - 'viewreset', - 'movestart', - 'move', - 'moveend', - 'dragstart', - 'drag', - 'dragend', - 'zoomstart', - 'zoomend', - 'zoomlevelschange', - 'resize', - 'autopanstart', - 'layeradd', - 'layerremove', - 'baselayerchange', - 'overlayadd', - 'overlayremove', - 'locationfound', - 'locationerror', - 'popupopen', - 'popupclose', - 'draw:created', - 'draw:edited', - 'draw:deleted', - 'draw:drawstart', - 'draw:drawstop', - 'draw:editstart', - 'draw:editstop', - 'draw:deletestart', - 'draw:deletestop' - ]; - }, - genDispatchMapEvent: function (scope, eventName, logic) { - return function (e) { - // Put together broadcast name - var broadcastName = 'leafletDirectiveMap.' + eventName; - // Safely broadcast the event - safeApply(scope, function (scope) { - if (logic === 'emit') { - scope.$emit(broadcastName, { leafletEvent: e }); - } else if (logic === 'broadcast') { - $rootScope.$broadcast(broadcastName, { leafletEvent: e }); - } - }); - }; - }, - getAvailableMarkerEvents: _getAvailableMarkerEvents, - getAvailablePathEvents: _getAvailablePathEvents, - notifyCenterChangedToBounds: function (scope) { - scope.$broadcast('boundsChanged'); - }, - notifyCenterUrlHashChanged: function (scope, map, attrs, search) { - if (!isDefined(attrs.urlHashCenter)) { - return; - } - var center = map.getCenter(); - var centerUrlHash = center.lat.toFixed(4) + ':' + center.lng.toFixed(4) + ':' + map.getZoom(); - if (!isDefined(search.c) || search.c !== centerUrlHash) { - //$log.debug("notified new center..."); - scope.$emit('centerUrlHash', centerUrlHash); - } - }, - bindMarkerEvents: function (marker, name, markerData, leafletScope) { - var markerEvents = []; - var i; - var eventName; - var logic = 'broadcast'; - if (!isDefined(leafletScope.eventBroadcast)) { - // Backward compatibility, if no event-broadcast attribute, all events are broadcasted - markerEvents = _getAvailableMarkerEvents(); - } else if (!isObject(leafletScope.eventBroadcast)) { - // Not a valid object - $log.error('[AngularJS - Leaflet] event-broadcast must be an object check your model.'); - } else { - // We have a possible valid object - if (!isDefined(leafletScope.eventBroadcast.marker)) { - // We do not have events enable/disable do we do nothing (all enabled by default) - markerEvents = _getAvailableMarkerEvents(); - } else if (!isObject(leafletScope.eventBroadcast.marker)) { - // Not a valid object - $log.warn('[AngularJS - Leaflet] event-broadcast.marker must be an object check your model.'); - } else { - // We have a possible valid map object - // Event propadation logic - if (leafletScope.eventBroadcast.marker.logic !== undefined && leafletScope.eventBroadcast.marker.logic !== null) { - // We take care of possible propagation logic - if (leafletScope.eventBroadcast.marker.logic !== 'emit' && leafletScope.eventBroadcast.marker.logic !== 'broadcast') { - // This is an error - $log.warn('[AngularJS - Leaflet] Available event propagation logic are: \'emit\' or \'broadcast\'.'); - } else if (leafletScope.eventBroadcast.marker.logic === 'emit') { - logic = 'emit'; - } - } - // Enable / Disable - var markerEventsEnable = false, markerEventsDisable = false; - if (leafletScope.eventBroadcast.marker.enable !== undefined && leafletScope.eventBroadcast.marker.enable !== null) { - if (typeof leafletScope.eventBroadcast.marker.enable === 'object') { - markerEventsEnable = true; - } - } - if (leafletScope.eventBroadcast.marker.disable !== undefined && leafletScope.eventBroadcast.marker.disable !== null) { - if (typeof leafletScope.eventBroadcast.marker.disable === 'object') { - markerEventsDisable = true; - } - } - if (markerEventsEnable && markerEventsDisable) { - // Both are active, this is an error - $log.warn('[AngularJS - Leaflet] can not enable and disable events at the same time'); - } else if (!markerEventsEnable && !markerEventsDisable) { - // Both are inactive, this is an error - $log.warn('[AngularJS - Leaflet] must enable or disable events'); - } else { - // At this point the marker object is OK, lets enable or disable events - if (markerEventsEnable) { - // Enable events - for (i = 0; i < leafletScope.eventBroadcast.marker.enable.length; i++) { - eventName = leafletScope.eventBroadcast.marker.enable[i]; - // Do we have already the event enabled? - if (markerEvents.indexOf(eventName) !== -1) { - // Repeated event, this is an error - $log.warn('[AngularJS - Leaflet] This event ' + eventName + ' is already enabled'); - } else { - // Does the event exists? - if (_getAvailableMarkerEvents().indexOf(eventName) === -1) { - // The event does not exists, this is an error - $log.warn('[AngularJS - Leaflet] This event ' + eventName + ' does not exist'); - } else { - // All ok enable the event - markerEvents.push(eventName); - } - } - } - } else { - // Disable events - markerEvents = _getAvailableMarkerEvents(); - for (i = 0; i < leafletScope.eventBroadcast.marker.disable.length; i++) { - eventName = leafletScope.eventBroadcast.marker.disable[i]; - var index = markerEvents.indexOf(eventName); - if (index === -1) { - // The event does not exist - $log.warn('[AngularJS - Leaflet] This event ' + eventName + ' does not exist or has been already disabled'); - } else { - markerEvents.splice(index, 1); - } - } - } - } - } - } - for (i = 0; i < markerEvents.length; i++) { - eventName = markerEvents[i]; - marker.on(eventName, genDispatchMarkerEvent(eventName, logic, leafletScope, marker, name, markerData)); - } - if (Helpers.LabelPlugin.isLoaded() && isDefined(marker.label)) { - genLabelEvents(leafletScope, logic, marker, name); - } - }, - bindPathEvents: function (path, name, pathData, leafletScope) { - var pathEvents = []; - var i; - var eventName; - var logic = 'broadcast'; - window.lls = leafletScope; - if (!isDefined(leafletScope.eventBroadcast)) { - // Backward compatibility, if no event-broadcast attribute, all events are broadcasted - pathEvents = _getAvailablePathEvents(); - } else if (!isObject(leafletScope.eventBroadcast)) { - // Not a valid object - $log.error('[AngularJS - Leaflet] event-broadcast must be an object check your model.'); - } else { - // We have a possible valid object - if (!isDefined(leafletScope.eventBroadcast.path)) { - // We do not have events enable/disable do we do nothing (all enabled by default) - pathEvents = _getAvailablePathEvents(); - } else if (isObject(leafletScope.eventBroadcast.paths)) { - // Not a valid object - $log.warn('[AngularJS - Leaflet] event-broadcast.path must be an object check your model.'); - } else { - // We have a possible valid map object - // Event propadation logic - if (leafletScope.eventBroadcast.path.logic !== undefined && leafletScope.eventBroadcast.path.logic !== null) { - // We take care of possible propagation logic - if (leafletScope.eventBroadcast.path.logic !== 'emit' && leafletScope.eventBroadcast.path.logic !== 'broadcast') { - // This is an error - $log.warn('[AngularJS - Leaflet] Available event propagation logic are: \'emit\' or \'broadcast\'.'); - } else if (leafletScope.eventBroadcast.path.logic === 'emit') { - logic = 'emit'; - } - } - // Enable / Disable - var pathEventsEnable = false, pathEventsDisable = false; - if (leafletScope.eventBroadcast.path.enable !== undefined && leafletScope.eventBroadcast.path.enable !== null) { - if (typeof leafletScope.eventBroadcast.path.enable === 'object') { - pathEventsEnable = true; - } - } - if (leafletScope.eventBroadcast.path.disable !== undefined && leafletScope.eventBroadcast.path.disable !== null) { - if (typeof leafletScope.eventBroadcast.path.disable === 'object') { - pathEventsDisable = true; - } - } - if (pathEventsEnable && pathEventsDisable) { - // Both are active, this is an error - $log.warn('[AngularJS - Leaflet] can not enable and disable events at the same time'); - } else if (!pathEventsEnable && !pathEventsDisable) { - // Both are inactive, this is an error - $log.warn('[AngularJS - Leaflet] must enable or disable events'); - } else { - // At this point the path object is OK, lets enable or disable events - if (pathEventsEnable) { - // Enable events - for (i = 0; i < leafletScope.eventBroadcast.path.enable.length; i++) { - eventName = leafletScope.eventBroadcast.path.enable[i]; - // Do we have already the event enabled? - if (pathEvents.indexOf(eventName) !== -1) { - // Repeated event, this is an error - $log.warn('[AngularJS - Leaflet] This event ' + eventName + ' is already enabled'); - } else { - // Does the event exists? - if (_getAvailablePathEvents().indexOf(eventName) === -1) { - // The event does not exists, this is an error - $log.warn('[AngularJS - Leaflet] This event ' + eventName + ' does not exist'); - } else { - // All ok enable the event - pathEvents.push(eventName); - } - } - } - } else { - // Disable events - pathEvents = _getAvailablePathEvents(); - for (i = 0; i < leafletScope.eventBroadcast.path.disable.length; i++) { - eventName = leafletScope.eventBroadcast.path.disable[i]; - var index = pathEvents.indexOf(eventName); - if (index === -1) { - // The event does not exist - $log.warn('[AngularJS - Leaflet] This event ' + eventName + ' does not exist or has been already disabled'); - } else { - pathEvents.splice(index, 1); - } - } - } - } - } - } - for (i = 0; i < pathEvents.length; i++) { - eventName = pathEvents[i]; - path.on(eventName, genDispatchPathEvent(eventName, logic, leafletScope, pathEvents, name)); - } - if (Helpers.LabelPlugin.isLoaded() && isDefined(path.label)) { - genLabelEvents(leafletScope, logic, path, name); - } - } - }; - } - ]); - angular.module('leaflet-directive').factory('leafletLayerHelpers', [ - '$rootScope', - '$log', - 'leafletHelpers', - function ($rootScope, $log, leafletHelpers) { - var Helpers = leafletHelpers, isString = leafletHelpers.isString, isObject = leafletHelpers.isObject, isDefined = leafletHelpers.isDefined; - var layerTypes = { - xyz: { - mustHaveUrl: true, - createLayer: function (params) { - return L.tileLayer(params.url, params.options); - } - }, - geoJSON: { - mustHaveUrl: true, - createLayer: function (params) { - if (!Helpers.GeoJSONPlugin.isLoaded()) { - return; - } - return new L.TileLayer.GeoJSON(params.url, params.pluginOptions, params.options); - } - }, - wms: { - mustHaveUrl: true, - createLayer: function (params) { - return L.tileLayer.wms(params.url, params.options); - } - }, - wmts: { - mustHaveUrl: true, - createLayer: function (params) { - return L.tileLayer.wmts(params.url, params.options); - } - }, - wfs: { - mustHaveUrl: true, - mustHaveLayer: true, - createLayer: function (params) { - if (!Helpers.WFSLayerPlugin.isLoaded()) { - return; - } - var options = angular.copy(params.options); - if (options.crs && 'string' === typeof options.crs) { - /*jshint -W061 */ - options.crs = eval(options.crs); - } - return new L.GeoJSON.WFS(params.url, params.layer, options); - } - }, - group: { - mustHaveUrl: false, - createLayer: function () { - return L.layerGroup(); - } - }, - google: { - mustHaveUrl: false, - createLayer: function (params) { - var type = params.type || 'SATELLITE'; - if (!Helpers.GoogleLayerPlugin.isLoaded()) { - return; - } - return new L.Google(type, params.options); - } - }, - china: { - mustHaveUrl: false, - createLayer: function (params) { - var type = params.type || ''; - if (!Helpers.ChinaLayerPlugin.isLoaded()) { - return; - } - return L.tileLayer.chinaProvider(type, params.options); - } - }, - ags: { - mustHaveUrl: true, - createLayer: function (params) { - if (!Helpers.AGSLayerPlugin.isLoaded()) { - return; - } - var options = angular.copy(params.options); - angular.extend(options, { url: params.url }); - var layer = new lvector.AGS(options); - layer.onAdd = function (map) { - this.setMap(map); - }; - layer.onRemove = function () { - this.setMap(null); - }; - return layer; - } - }, - dynamic: { - mustHaveUrl: true, - createLayer: function (params) { - if (!Helpers.DynamicMapLayerPlugin.isLoaded()) { - return; - } - return L.esri.dynamicMapLayer(params.url, params.options); - } - }, - markercluster: { - mustHaveUrl: false, - createLayer: function (params) { - if (!Helpers.MarkerClusterPlugin.isLoaded()) { - $log.error('[AngularJS - Leaflet] The markercluster plugin is not loaded.'); - return; - } - return new L.MarkerClusterGroup(params.options); - } - }, - bing: { - mustHaveUrl: false, - createLayer: function (params) { - if (!Helpers.BingLayerPlugin.isLoaded()) { - return; - } - return new L.BingLayer(params.key, params.options); - } - }, - heatmap: { - mustHaveUrl: false, - mustHaveData: true, - createLayer: function (params) { - if (!Helpers.HeatMapLayerPlugin.isLoaded()) { - return; - } - var layer = new L.TileLayer.WebGLHeatMap(params.options); - if (isDefined(params.data)) { - layer.setData(params.data); - } - return layer; - } - }, - yandex: { - mustHaveUrl: false, - createLayer: function (params) { - var type = params.type || 'map'; - if (!Helpers.YandexLayerPlugin.isLoaded()) { - return; - } - return new L.Yandex(type, params.options); - } - }, - imageOverlay: { - mustHaveUrl: true, - mustHaveBounds: true, - createLayer: function (params) { - return L.imageOverlay(params.url, params.bounds, params.options); - } - } - }; - function isValidLayerType(layerDefinition) { - // Check if the baselayer has a valid type - if (!isString(layerDefinition.type)) { - return false; - } - if (Object.keys(layerTypes).indexOf(layerDefinition.type) === -1) { - $log.error('[AngularJS - Leaflet] A layer must have a valid type: ' + Object.keys(layerTypes)); - return false; - } - // Check if the layer must have an URL - if (layerTypes[layerDefinition.type].mustHaveUrl && !isString(layerDefinition.url)) { - $log.error('[AngularJS - Leaflet] A base layer must have an url'); - return false; - } - if (layerTypes[layerDefinition.type].mustHaveData && !isDefined(layerDefinition.data)) { - $log.error('[AngularJS - Leaflet] The base layer must have a "data" array attribute'); - return false; - } - if (layerTypes[layerDefinition.type].mustHaveLayer && !isDefined(layerDefinition.layer)) { - $log.error('[AngularJS - Leaflet] The type of layer ' + layerDefinition.type + ' must have an layer defined'); - return false; - } - if (layerTypes[layerDefinition.type].mustHaveBounds && !isDefined(layerDefinition.bounds)) { - $log.error('[AngularJS - Leaflet] The type of layer ' + layerDefinition.type + ' must have bounds defined'); - return false; - } - return true; - } - return { - createLayer: function (layerDefinition) { - if (!isValidLayerType(layerDefinition)) { - return; - } - if (!isString(layerDefinition.name)) { - $log.error('[AngularJS - Leaflet] A base layer must have a name'); - return; - } - if (!isObject(layerDefinition.layerParams)) { - layerDefinition.layerParams = {}; - } - if (!isObject(layerDefinition.layerOptions)) { - layerDefinition.layerOptions = {}; - } - // Mix the layer specific parameters with the general Leaflet options. Although this is an overhead - // the definition of a base layers is more 'clean' if the two types of parameters are differentiated - for (var attrname in layerDefinition.layerParams) { - layerDefinition.layerOptions[attrname] = layerDefinition.layerParams[attrname]; - } - var params = { - url: layerDefinition.url, - data: layerDefinition.data, - options: layerDefinition.layerOptions, - layer: layerDefinition.layer, - type: layerDefinition.layerType, - bounds: layerDefinition.bounds, - key: layerDefinition.key, - pluginOptions: layerDefinition.pluginOptions - }; - //TODO Add $watch to the layer properties - return layerTypes[layerDefinition.type].createLayer(params); - } - }; - } - ]); - angular.module('leaflet-directive').factory('leafletControlHelpers', [ - '$rootScope', - '$log', - 'leafletHelpers', - 'leafletMapDefaults', - function ($rootScope, $log, leafletHelpers, leafletMapDefaults) { - var isObject = leafletHelpers.isObject, isDefined = leafletHelpers.isDefined; - var _layersControl; - var _controlLayersMustBeVisible = function (baselayers, overlays) { - var numberOfLayers = 0; - if (isObject(baselayers)) { - numberOfLayers += Object.keys(baselayers).length; - } - if (isObject(overlays)) { - numberOfLayers += Object.keys(overlays).length; - } - return numberOfLayers > 1; - }; - var _createLayersControl = function (mapId) { - var defaults = leafletMapDefaults.getDefaults(mapId); - var controlOptions = { - collapsed: defaults.controls.layers.collapsed, - position: defaults.controls.layers.position - }; - var control; - if (defaults.controls.layers && isDefined(defaults.controls.layers.control)) { - control = defaults.controls.layers.control.apply(this, [ - [], - [], - controlOptions - ]); - } else { - control = new L.control.layers([], [], controlOptions); - } - return control; - }; - return { - layersControlMustBeVisible: _controlLayersMustBeVisible, - updateLayersControl: function (map, mapId, loaded, baselayers, overlays, leafletLayers) { - var i; - var mustBeLoaded = _controlLayersMustBeVisible(baselayers, overlays); - if (isDefined(_layersControl) && loaded) { - for (i in leafletLayers.baselayers) { - _layersControl.removeLayer(leafletLayers.baselayers[i]); - } - for (i in leafletLayers.overlays) { - _layersControl.removeLayer(leafletLayers.overlays[i]); - } - _layersControl.removeFrom(map); - } - if (mustBeLoaded) { - _layersControl = _createLayersControl(mapId); - for (i in baselayers) { - if (isDefined(leafletLayers.baselayers[i])) { - _layersControl.addBaseLayer(leafletLayers.baselayers[i], baselayers[i].name); - } - } - for (i in overlays) { - if (isDefined(leafletLayers.overlays[i])) { - _layersControl.addOverlay(leafletLayers.overlays[i], overlays[i].name); - } - } - _layersControl.addTo(map); - } - return mustBeLoaded; - } - }; - } - ]); - angular.module('leaflet-directive').factory('leafletLegendHelpers', function () { - var _updateArcGISLegend = function (div, legendData) { - div.innerHTML = ''; - if (legendData.error) { - div.innerHTML += '
        ' + legendData.error.message + '
        '; - } else { - for (var i = 0; i < legendData.layers.length; i++) { - var layer = legendData.layers[i]; - div.innerHTML += '
        ' + layer.layerName + '
        '; - for (var j = 0; j < layer.legend.length; j++) { - var leg = layer.legend[j]; - div.innerHTML += '
        ' + '
        ' + leg.label + '
        '; - } - } - } - }; - var _getOnAddArcGISLegend = function (legendData, legendClass) { - return function () { - var div = L.DomUtil.create('div', legendClass); - if (!L.Browser.touch) { - L.DomEvent.disableClickPropagation(div); - L.DomEvent.on(div, 'mousewheel', L.DomEvent.stopPropagation); - } else { - L.DomEvent.on(div, 'click', L.DomEvent.stopPropagation); - } - _updateArcGISLegend(div, legendData); - return div; - }; - }; - var _getOnAddArrayLegend = function (legend, legendClass) { - return function () { - var div = L.DomUtil.create('div', legendClass); - for (var i = 0; i < legend.colors.length; i++) { - div.innerHTML += '
        ' + '
        ' + legend.labels[i] + '
        '; - } - if (!L.Browser.touch) { - L.DomEvent.disableClickPropagation(div); - L.DomEvent.on(div, 'mousewheel', L.DomEvent.stopPropagation); - } else { - L.DomEvent.on(div, 'click', L.DomEvent.stopPropagation); - } - return div; - }; - }; - return { - getOnAddArcGISLegend: _getOnAddArcGISLegend, - getOnAddArrayLegend: _getOnAddArrayLegend, - updateArcGISLegend: _updateArcGISLegend - }; - }); - angular.module('leaflet-directive').factory('leafletPathsHelpers', [ - '$rootScope', - '$log', - 'leafletHelpers', - function ($rootScope, $log, leafletHelpers) { - var isDefined = leafletHelpers.isDefined, isArray = leafletHelpers.isArray, isNumber = leafletHelpers.isNumber, isValidPoint = leafletHelpers.isValidPoint; - function _convertToLeafletLatLngs(latlngs) { - return latlngs.filter(function (latlng) { - return isValidPoint(latlng); - }).map(function (latlng) { - return new L.LatLng(latlng.lat, latlng.lng); }); - } - function _convertToLeafletLatLng(latlng) { - return new L.LatLng(latlng.lat, latlng.lng); - } - function _convertToLeafletMultiLatLngs(paths) { - return paths.map(function (latlngs) { - return _convertToLeafletLatLngs(latlngs); - }); - } - function _getOptions(path, defaults) { - var availableOptions = [ - 'stroke', - 'weight', - 'color', - 'opacity', - 'fill', - 'fillColor', - 'fillOpacity', - 'dashArray', - 'lineCap', - 'lineJoin', - 'clickable', - 'pointerEvents', - 'className', - 'smoothFactor', - 'noClip' - ]; - var options = {}; - for (var i = 0; i < availableOptions.length; i++) { - var optionName = availableOptions[i]; - if (isDefined(path[optionName])) { - options[optionName] = path[optionName]; - } else if (isDefined(defaults.path[optionName])) { - options[optionName] = defaults.path[optionName]; - } - } - return options; - } - var _updatePathOptions = function (path, data) { - if (isDefined(data.weight)) { - path.setStyle({ weight: data.weight }); - } - if (isDefined(data.color)) { - path.setStyle({ color: data.color }); - } - if (isDefined(data.opacity)) { - path.setStyle({ opacity: data.opacity }); - } - }; - var _isValidPolyline = function (latlngs) { - if (!isArray(latlngs)) { - return false; - } - for (var i in latlngs) { - var point = latlngs[i]; - if (!isValidPoint(point)) { - return false; - } - } - return true; - }; - var pathTypes = { - polyline: { - isValid: function (pathData) { - var latlngs = pathData.latlngs; - return _isValidPolyline(latlngs); - }, - createPath: function (options) { - return new L.Polyline([], options); - }, - setPath: function (path, data) { - path.setLatLngs(_convertToLeafletLatLngs(data.latlngs)); - _updatePathOptions(path, data); - return; - } - }, - multiPolyline: { - isValid: function (pathData) { - var latlngs = pathData.latlngs; - if (!isArray(latlngs)) { - return false; - } - for (var i in latlngs) { - var polyline = latlngs[i]; - if (!_isValidPolyline(polyline)) { - return false; - } - } - return true; - }, - createPath: function (options) { - return new L.multiPolyline([[ - [ - 0, - 0 - ], - [ - 1, - 1 - ] - ]], options); - }, - setPath: function (path, data) { - path.setLatLngs(_convertToLeafletMultiLatLngs(data.latlngs)); - _updatePathOptions(path, data); - return; - } - }, - polygon: { - isValid: function (pathData) { - var latlngs = pathData.latlngs; - return _isValidPolyline(latlngs); - }, - createPath: function (options) { - return new L.Polygon([], options); - }, - setPath: function (path, data) { - path.setLatLngs(_convertToLeafletLatLngs(data.latlngs)); - _updatePathOptions(path, data); - return; - } - }, - multiPolygon: { - isValid: function (pathData) { - var latlngs = pathData.latlngs; - if (!isArray(latlngs)) { - return false; - } - for (var i in latlngs) { - var polyline = latlngs[i]; - if (!_isValidPolyline(polyline)) { - return false; - } - } - return true; - }, - createPath: function (options) { - return new L.MultiPolygon([[ - [ - 0, - 0 - ], - [ - 1, - 1 - ], - [ - 0, - 1 - ] - ]], options); - }, - setPath: function (path, data) { - path.setLatLngs(_convertToLeafletMultiLatLngs(data.latlngs)); - _updatePathOptions(path, data); - return; - } - }, - rectangle: { - isValid: function (pathData) { - var latlngs = pathData.latlngs; - if (!isArray(latlngs) || latlngs.length !== 2) { - return false; - } - for (var i in latlngs) { - var point = latlngs[i]; - if (!isValidPoint(point)) { - return false; - } - } - return true; - }, - createPath: function (options) { - return new L.Rectangle([ - [ - 0, - 0 - ], - [ - 1, - 1 - ] - ], options); - }, - setPath: function (path, data) { - path.setBounds(new L.LatLngBounds(_convertToLeafletLatLngs(data.latlngs))); - _updatePathOptions(path, data); - } - }, - circle: { - isValid: function (pathData) { - var point = pathData.latlngs; - return isValidPoint(point) && isNumber(pathData.radius); - }, - createPath: function (options) { - return new L.Circle([ - 0, - 0 - ], 1, options); - }, - setPath: function (path, data) { - path.setLatLng(_convertToLeafletLatLng(data.latlngs)); - if (isDefined(data.radius)) { - path.setRadius(data.radius); - } - _updatePathOptions(path, data); - } - }, - circleMarker: { - isValid: function (pathData) { - var point = pathData.latlngs; - return isValidPoint(point) && isNumber(pathData.radius); - }, - createPath: function (options) { - return new L.CircleMarker([ - 0, - 0 - ], options); - }, - setPath: function (path, data) { - path.setLatLng(_convertToLeafletLatLng(data.latlngs)); - if (isDefined(data.radius)) { - path.setRadius(data.radius); - } - _updatePathOptions(path, data); - } - } - }; - var _getPathData = function (path) { - var pathData = {}; - if (path.latlngs) { - pathData.latlngs = path.latlngs; - } - if (path.radius) { - pathData.radius = path.radius; - } - return pathData; - }; - return { - setPathOptions: function (leafletPath, pathType, data) { - if (!isDefined(pathType)) { - pathType = 'polyline'; - } - pathTypes[pathType].setPath(leafletPath, data); - }, - createPath: function (name, path, defaults) { - if (!isDefined(path.type)) { - path.type = 'polyline'; - } - var options = _getOptions(path, defaults); - var pathData = _getPathData(path); - if (!pathTypes[path.type].isValid(pathData)) { - $log.error('[AngularJS - Leaflet] Invalid data passed to the ' + path.type + ' path'); - return; - } - return pathTypes[path.type].createPath(options); - } - }; - } - ]); - angular.module('leaflet-directive').factory('leafletBoundsHelpers', [ - '$log', - 'leafletHelpers', - function ($log, leafletHelpers) { - var isArray = leafletHelpers.isArray, isNumber = leafletHelpers.isNumber; - function _isValidBounds(bounds) { - return angular.isDefined(bounds) && angular.isDefined(bounds.southWest) && angular.isDefined(bounds.northEast) && angular.isNumber(bounds.southWest.lat) && angular.isNumber(bounds.southWest.lng) && angular.isNumber(bounds.northEast.lat) && angular.isNumber(bounds.northEast.lng); - } - return { - createLeafletBounds: function (bounds) { - if (_isValidBounds(bounds)) { - return L.latLngBounds([ - bounds.southWest.lat, - bounds.southWest.lng - ], [ - bounds.northEast.lat, - bounds.northEast.lng - ]); - } - }, - isValidBounds: _isValidBounds, - createBoundsFromArray: function (boundsArray) { - if (!(isArray(boundsArray) && boundsArray.length === 2 && isArray(boundsArray[0]) && isArray(boundsArray[1]) && boundsArray[0].length === 2 && boundsArray[1].length === 2 && isNumber(boundsArray[0][0]) && isNumber(boundsArray[0][1]) && isNumber(boundsArray[1][0]) && isNumber(boundsArray[1][1]))) { - $log.error('[AngularJS - Leaflet] The bounds array is not valid.'); - return; - } - return { - northEast: { - lat: boundsArray[0][0], - lng: boundsArray[0][1] - }, - southWest: { - lat: boundsArray[1][0], - lng: boundsArray[1][1] - } + }); + }, + }; +}]); + +angular.module('leaflet-directive').directive('paths', ["$log", "$q", "leafletData", "leafletMapDefaults", "leafletHelpers", "leafletPathsHelpers", "leafletPathEvents", function($log, $q, leafletData, leafletMapDefaults, leafletHelpers, leafletPathsHelpers, leafletPathEvents) { + + return { + restrict: 'A', + scope: false, + replace: false, + require: ['leaflet', '?layers'], + + link: function(scope, element, attrs, controller) { + var mapController = controller[0]; + var isDefined = leafletHelpers.isDefined; + var isString = leafletHelpers.isString; + var leafletScope = mapController.getLeafletScope(); + var paths = leafletScope.paths; + var createPath = leafletPathsHelpers.createPath; + var bindPathEvents = leafletPathEvents.bindPathEvents; + var setPathOptions = leafletPathsHelpers.setPathOptions; + + mapController.getMap().then(function(map) { + var defaults = leafletMapDefaults.getDefaults(attrs.id); + var getLayers; + + // If the layers attribute is used, we must wait until the layers are created + if (isDefined(controller[1])) { + getLayers = controller[1].getLayers; + } else { + getLayers = function() { + var deferred = $q.defer(); + deferred.resolve(); + return deferred.promise; }; } - }; - } - ]); - angular.module('leaflet-directive').factory('leafletMarkersHelpers', [ - '$rootScope', - 'leafletHelpers', - '$log', - function ($rootScope, leafletHelpers, $log) { - var isDefined = leafletHelpers.isDefined, MarkerClusterPlugin = leafletHelpers.MarkerClusterPlugin, AwesomeMarkersPlugin = leafletHelpers.AwesomeMarkersPlugin, safeApply = leafletHelpers.safeApply, Helpers = leafletHelpers, isString = leafletHelpers.isString, isNumber = leafletHelpers.isNumber, isObject = leafletHelpers.isObject, groups = {}; - var createLeafletIcon = function (iconData) { - if (isDefined(iconData) && isDefined(iconData.type) && iconData.type === 'awesomeMarker') { - if (!AwesomeMarkersPlugin.isLoaded()) { - $log.error('[AngularJS - Leaflet] The AwesomeMarkers Plugin is not loaded.'); - } - return new L.AwesomeMarkers.icon(iconData); + + if (!isDefined(paths)) { + return; } - if (isDefined(iconData) && isDefined(iconData.type) && iconData.type === 'div') { - return new L.divIcon(iconData); - } - var base64icon = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABkAAAApCAYAAADAk4LOAAAGmklEQVRYw7VXeUyTZxjvNnfELFuyIzOabermMZEeQC/OclkO49CpOHXOLJl/CAURuYbQi3KLgEhbrhZ1aDwmaoGqKII6odATmH/scDFbdC7LvFqOCc+e95s2VG50X/LLm/f4/Z7neY/ne18aANCmAr5E/xZf1uDOkTcGcWR6hl9247tT5U7Y6SNvWsKT63P58qbfeLJG8M5qcgTknrvvrdDbsT7Ml+tv82X6vVxJE33aRmgSyYtcWVMqX97Yv2JvW39UhRE2HuyBL+t+gK1116ly06EeWFNlAmHxlQE0OMiV6mQCScusKRlhS3QLeVJdl1+23h5dY4FNB3thrbYboqptEFlphTC1hSpJnbRvxP4NWgsE5Jyz86QNNi/5qSUTGuFk1gu54tN9wuK2wc3o+Wc13RCmsoBwEqzGcZsxsvCSy/9wJKf7UWf1mEY8JWfewc67UUoDbDjQC+FqK4QqLVMGGR9d2wurKzqBk3nqIT/9zLxRRjgZ9bqQgub+DdoeCC03Q8j+0QhFhBHR/eP3U/zCln7Uu+hihJ1+bBNffLIvmkyP0gpBZWYXhKussK6mBz5HT6M1Nqpcp+mBCPXosYQfrekGvrjewd59/GvKCE7TbK/04/ZV5QZYVWmDwH1mF3xa2Q3ra3DBC5vBT1oP7PTj4C0+CcL8c7C2CtejqhuCnuIQHaKHzvcRfZpnylFfXsYJx3pNLwhKzRAwAhEqG0SpusBHfAKkxw3w4627MPhoCH798z7s0ZnBJ/MEJbZSbXPhER2ih7p2ok/zSj2cEJDd4CAe+5WYnBCgR2uruyEw6zRoW6/DWJ/OeAP8pd/BGtzOZKpG8oke0SX6GMmRk6GFlyAc59K32OTEinILRJRchah8HQwND8N435Z9Z0FY1EqtxUg+0SO6RJ/mmXz4VuS+DpxXC3gXmZwIL7dBSH4zKE50wESf8qwVgrP1EIlTO5JP9Igu0aexdh28F1lmAEGJGfh7jE6ElyM5Rw/FDcYJjWhbeiBYoYNIpc2FT/SILivp0F1ipDWk4BIEo2VuodEJUifhbiltnNBIXPUFCMpthtAyqws/BPlEF/VbaIxErdxPphsU7rcCp8DohC+GvBIPJS/tW2jtvTmmAeuNO8BNOYQeG8G/2OzCJ3q+soYB5i6NhMaKr17FSal7GIHheuV3uSCY8qYVuEm1cOzqdWr7ku/R0BDoTT+DT+ohCM6/CCvKLKO4RI+dXPeAuaMqksaKrZ7L3FE5FIFbkIceeOZ2OcHO6wIhTkNo0ffgjRGxEqogXHYUPHfWAC/lADpwGcLRY3aeK4/oRGCKYcZXPVoeX/kelVYY8dUGf8V5EBRbgJXT5QIPhP9ePJi428JKOiEYhYXFBqou2Guh+p/mEB1/RfMw6rY7cxcjTrneI1FrDyuzUSRm9miwEJx8E/gUmqlyvHGkneiwErR21F3tNOK5Tf0yXaT+O7DgCvALTUBXdM4YhC/IawPU+2PduqMvuaR6eoxSwUk75ggqsYJ7VicsnwGIkZBSXKOUww73WGXyqP+J2/b9c+gi1YAg/xpwck3gJuucNrh5JvDPvQr0WFXf0piyt8f8/WI0hV4pRxxkQZdJDfDJNOAmM0Ag8jyT6hz0WGXWuP94Yh2jcfjmXAGvHCMslRimDHYuHuDsy2QtHuIavznhbYURq5R57KpzBBRZKPJi8eQg48h4j8SDdowifdIrEVdU+gbO6QNvRRt4ZBthUaZhUnjlYObNagV3keoeru3rU7rcuceqU1mJBxy+BWZYlNEBH+0eH4vRiB+OYybU2hnblYlTvkHinM4m54YnxSyaZYSF6R3jwgP7udKLGIX6r/lbNa9N6y5MFynjWDtrHd75ZvTYAPO/6RgF0k76mQla3FGq7dO+cH8sKn0Vo7nDllwAhqwLPkxrHwWmHJOo+AKJ4rab5OgrM7rVu8eWb2Pu0Dh4eDgXoOfvp7Y7QeqknRmvcTBEyq9m/HQQSCSz6LHq3z0yzsNySRfMS253wl2KyRDbcZPcfJKjZmSEOjcxyi+Y8dUOtsIEH6R2wNykdqrkYJ0RV92H0W58pkfQk7cKevsLK10Py8SdMGfXNXATY+pPbyJR/ET6n9nIfztNtZYRV9XniQu9IA2vOVgy4ir7GCLVmmd+zjkH0eAF9Po6K61pmCXHxU5rHMYd1ftc3owjwRSVRzLjKvqZEty6cRUD7jGqiOdu5HG6MdHjNcNYGqfDm5YRzLBBCCDl/2bk8a8gdbqcfwECu62Fg/HrggAAAABJRU5ErkJggg=='; - var base64shadow = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACkAAAApCAYAAACoYAD2AAAC5ElEQVRYw+2YW4/TMBCF45S0S1luXZCABy5CgLQgwf//S4BYBLTdJLax0fFqmB07nnQfEGqkIydpVH85M+NLjPe++dcPc4Q8Qh4hj5D/AaQJx6H/4TMwB0PeBNwU7EGQAmAtsNfAzoZkgIa0ZgLMa4Aj6CxIAsjhjOCoL5z7Glg1JAOkaicgvQBXuncwJAWjksLtBTWZe04CnYRktUGdilALppZBOgHGZcBzL6OClABvMSVIzyBjazOgrvACf1ydC5mguqAVg6RhdkSWQFj2uxfaq/BrIZOLEWgZdALIDvcMcZLD8ZbLC9de4yR1sYMi4G20S4Q/PWeJYxTOZn5zJXANZHIxAd4JWhPIloTJZhzMQduM89WQ3MUVAE/RnhAXpTycqys3NZALOBbB7kFrgLesQl2h45Fcj8L1tTSohUwuxhy8H/Qg6K7gIs+3kkaigQCOcyEXCHN07wyQazhrmIulvKMQAwMcmLNqyCVyMAI+BuxSMeTk3OPikLY2J1uE+VHQk6ANrhds+tNARqBeaGc72cK550FP4WhXmFmcMGhTwAR1ifOe3EvPqIegFmF+C8gVy0OfAaWQPMR7gF1OQKqGoBjq90HPMP01BUjPOqGFksC4emE48tWQAH0YmvOgF3DST6xieJgHAWxPAHMuNhrImIdvoNOKNWIOcE+UXE0pYAnkX6uhWsgVXDxHdTfCmrEEmMB2zMFimLVOtiiajxiGWrbU52EeCdyOwPEQD8LqyPH9Ti2kgYMf4OhSKB7qYILbBv3CuVTJ11Y80oaseiMWOONc/Y7kJYe0xL2f0BaiFTxknHO5HaMGMublKwxFGzYdWsBF174H/QDknhTHmHHN39iWFnkZx8lPyM8WHfYELmlLKtgWNmFNzQcC1b47gJ4hL19i7o65dhH0Negbca8vONZoP7doIeOC9zXm8RjuL0Gf4d4OYaU5ljo3GYiqzrWQHfJxA6ALhDpVKv9qYeZA8eM3EhfPSCmpuD0AAAAASUVORK5CYII='; - if (!isDefined(iconData)) { - return new L.Icon.Default({ - iconUrl: base64icon, - shadowUrl: base64shadow - }); - } - if (!isDefined(iconData.iconUrl)) { - iconData.iconUrl = base64icon; - iconData.shadowUrl = base64shadow; - } - return new L.Icon.Default(iconData); - }; - var _deleteMarker = function (marker, map, layers) { - marker.closePopup(); - // There is no easy way to know if a marker is added to a layer, so we search for it - // if there are overlays - if (isDefined(layers) && isDefined(layers.overlays)) { - for (var key in layers.overlays) { - if (layers.overlays[key] instanceof L.LayerGroup) { - if (layers.overlays[key].hasLayer(marker)) { - layers.overlays[key].removeLayer(marker); - return; - } - } - } - } - if (isDefined(groups)) { - for (var groupKey in groups) { - if (groups[groupKey].hasLayer(marker)) { - groups[groupKey].removeLayer(marker); - } - } - } - if (map.hasLayer(marker)) { - map.removeLayer(marker); - } - }; - return { - deleteMarker: _deleteMarker, - createMarker: function (markerData) { - if (!isDefined(markerData)) { - $log.error('[AngularJS - Leaflet] The marker definition is not valid.'); - return; - } - var markerOptions = { - icon: createLeafletIcon(markerData.icon), - title: isDefined(markerData.title) ? markerData.title : '', - draggable: isDefined(markerData.draggable) ? markerData.draggable : false, - clickable: isDefined(markerData.clickable) ? markerData.clickable : true, - riseOnHover: isDefined(markerData.riseOnHover) ? markerData.riseOnHover : false, - zIndexOffset: isDefined(markerData.zIndexOffset) ? markerData.zIndexOffset : 0, - iconAngle: isDefined(markerData.iconAngle) ? markerData.iconAngle : 0 - }; - return new L.marker(markerData, markerOptions); - }, - addMarkerToGroup: function (marker, groupName, map) { - if (!isString(groupName)) { - $log.error('[AngularJS - Leaflet] The marker group you have specified is invalid.'); - return; - } - if (!MarkerClusterPlugin.isLoaded()) { - $log.error('[AngularJS - Leaflet] The MarkerCluster plugin is not loaded.'); - return; - } - if (!isDefined(groups[groupName])) { - groups[groupName] = new L.MarkerClusterGroup(); - map.addLayer(groups[groupName]); - } - groups[groupName].addLayer(marker); - }, - listenMarkerEvents: function (marker, markerData, leafletScope) { - marker.on('popupopen', function () { - safeApply(leafletScope, function () { - markerData.focus = true; - }); - }); - marker.on('popupclose', function () { - safeApply(leafletScope, function () { - markerData.focus = false; - }); - }); - }, - addMarkerWatcher: function (marker, name, leafletScope, layers, map) { - var clearWatch = leafletScope.$watch('markers.' + name, function (markerData, oldMarkerData) { - if (!isDefined(markerData)) { - _deleteMarker(marker, map, layers); + + getLayers().then(function(layers) { + + var leafletPaths = {}; + leafletData.setPaths(leafletPaths, attrs.id); + + // Should we watch for every specific marker on the map? + var shouldWatch = (!isDefined(attrs.watchPaths) || attrs.watchPaths === 'true'); + + // Function for listening every single path once created + var watchPathFn = function(leafletPath, name) { + var clearWatch = leafletScope.$watch('paths["' + name + '"]', function(pathData, old) { + if (!isDefined(pathData)) { + if (isDefined(old.layer)) { + for (var i in layers.overlays) { + var overlay = layers.overlays[i]; + overlay.removeLayer(leafletPath); + } + } + + map.removeLayer(leafletPath); clearWatch(); return; } - if (!isDefined(oldMarkerData)) { - return; - } - // Update the lat-lng property (always present in marker properties) - if (!(isNumber(markerData.lat) && isNumber(markerData.lng))) { - $log.warn('There are problems with lat-lng data, please verify your marker model'); - _deleteMarker(marker, map, layers); - return; - } - // It is possible that the layer has been removed or the layer marker does not exist - // Update the layer group if present or move it to the map if not - if (!isString(markerData.layer)) { - // There is no layer information, we move the marker to the map if it was in a layer group - if (isString(oldMarkerData.layer)) { - // Remove from the layer group that is supposed to be - if (isDefined(layers.overlays[oldMarkerData.layer]) && layers.overlays[oldMarkerData.layer].hasLayer(marker)) { - layers.overlays[oldMarkerData.layer].removeLayer(marker); - marker.closePopup(); - } - // Test if it is not on the map and add it - if (!map.hasLayer(marker)) { - map.addLayer(marker); - } - } - } - if (isString(markerData.layer) && oldMarkerData.layer !== markerData.layer) { - // If it was on a layer group we have to remove it - if (isString(oldMarkerData.layer) && isDefined(layers.overlays[oldMarkerData.layer]) && layers.overlays[oldMarkerData.layer].hasLayer(marker)) { - layers.overlays[oldMarkerData.layer].removeLayer(marker); - } - marker.closePopup(); - // Remove it from the map in case the new layer is hidden or there is an error in the new layer - if (map.hasLayer(marker)) { - map.removeLayer(marker); - } - // The markerData.layer is defined so we add the marker to the layer if it is different from the old data - if (!isDefined(layers.overlays[markerData.layer])) { - $log.error('[AngularJS - Leaflet] You must use a name of an existing layer'); - return; - } - // Is a group layer? - var layerGroup = layers.overlays[markerData.layer]; - if (!(layerGroup instanceof L.LayerGroup)) { - $log.error('[AngularJS - Leaflet] A marker can only be added to a layer of type "group"'); - return; - } - // The marker goes to a correct layer group, so first of all we add it - layerGroup.addLayer(marker); - // The marker is automatically added to the map depending on the visibility - // of the layer, so we only have to open the popup if the marker is in the map - if (map.hasLayer(marker) && markerData.focus === true) { - marker.openPopup(); - } - } - // Update the draggable property - if (markerData.draggable !== true && oldMarkerData.draggable === true && isDefined(marker.dragging)) { - marker.dragging.disable(); - } - if (markerData.draggable === true && oldMarkerData.draggable !== true) { - // The markerData.draggable property must be true so we update if there wasn't a previous value or it wasn't true - if (marker.dragging) { - marker.dragging.enable(); - } else { - if (L.Handler.MarkerDrag) { - marker.dragging = new L.Handler.MarkerDrag(marker); - marker.options.draggable = true; - marker.dragging.enable(); - } - } - } - // Update the icon property - if (!isObject(markerData.icon)) { - // If there is no icon property or it's not an object - if (isObject(oldMarkerData.icon)) { - // If there was an icon before restore to the default - marker.setIcon(createLeafletIcon()); - marker.closePopup(); - marker.unbindPopup(); - if (isString(markerData.message)) { - marker.bindPopup(markerData.message); - } - } - } - if (isObject(markerData.icon) && isObject(oldMarkerData.icon) && !angular.equals(markerData.icon, oldMarkerData.icon)) { - var dragG = false; - if (marker.dragging) { - dragG = marker.dragging.enabled(); - } - marker.setIcon(createLeafletIcon(markerData.icon)); - if (dragG) { - marker.dragging.enable(); - } - marker.closePopup(); - marker.unbindPopup(); - if (isString(markerData.message)) { - marker.bindPopup(markerData.message); - } - } - // Update the Popup message property - if (!isString(markerData.message) && isString(oldMarkerData.message)) { - marker.closePopup(); - marker.unbindPopup(); - } - // Update the label content - if (Helpers.LabelPlugin.isLoaded() && isDefined(markerData.label) && isDefined(markerData.label.message) && !angular.equals(markerData.label.message, oldMarkerData.label.message)) { - marker.updateLabelContent(markerData.label.message); - } - // There is some text in the popup, so we must show the text or update existing - if (isString(markerData.message) && !isString(oldMarkerData.message)) { - // There was no message before so we create it - marker.bindPopup(markerData.message); - if (markerData.focus === true) { - // If the focus is set, we must open the popup, because we do not know if it was opened before - marker.openPopup(); - } - } - if (isString(markerData.message) && isString(oldMarkerData.message) && markerData.message !== oldMarkerData.message) { - // There was a different previous message so we update it - marker.setPopupContent(markerData.message); - } - // Update the focus property - var updatedFocus = false; - if (markerData.focus !== true && oldMarkerData.focus === true) { - // If there was a focus property and was true we turn it off - marker.closePopup(); - updatedFocus = true; - } - // The markerData.focus property must be true so we update if there wasn't a previous value or it wasn't true - if (markerData.focus === true && oldMarkerData.focus !== true) { - marker.openPopup(); - updatedFocus = true; - } - if (oldMarkerData.focus === true && markerData.focus === true) { - // Reopen the popup when focus is still true - marker.openPopup(); - updatedFocus = true; - } - var markerLatLng = marker.getLatLng(); - var isCluster = isString(markerData.layer) && Helpers.MarkerClusterPlugin.is(layers.overlays[markerData.layer]); - // If the marker is in a cluster it has to be removed and added to the layer when the location is changed - if (isCluster) { - // The focus has changed even by a user click or programatically - if (updatedFocus) { - // We only have to update the location if it was changed programatically, because it was - // changed by a user drag the marker data has already been updated by the internal event - // listened by the directive - if (markerData.lat !== oldMarkerData.lat || markerData.lng !== oldMarkerData.lng) { - layers.overlays[markerData.layer].removeLayer(marker); - marker.setLatLng([ - markerData.lat, - markerData.lng - ]); - layers.overlays[markerData.layer].addLayer(marker); - } - } else { - // The marker has possibly moved. It can be moved by a user drag (marker location and data are equal but old - // data is diferent) or programatically (marker location and data are diferent) - if (markerLatLng.lat !== markerData.lat || markerLatLng.lng !== markerData.lng) { - // The marker was moved by a user drag - layers.overlays[markerData.layer].removeLayer(marker); - marker.setLatLng([ - markerData.lat, - markerData.lng - ]); - layers.overlays[markerData.layer].addLayer(marker); - } else if (markerData.lat !== oldMarkerData.lat || markerData.lng !== oldMarkerData.lng) { - // The marker was moved programatically - layers.overlays[markerData.layer].removeLayer(marker); - marker.setLatLng([ - markerData.lat, - markerData.lng - ]); - layers.overlays[markerData.layer].addLayer(marker); - } - } - } else if (markerLatLng.lat !== markerData.lat || markerLatLng.lng !== markerData.lng) { - marker.setLatLng([ - markerData.lat, - markerData.lng - ]); - } + + setPathOptions(leafletPath, pathData.type, pathData); }, true); - } - }; - } - ]); - angular.module('leaflet-directive').factory('leafletHelpers', [ - '$q', - '$log', - function ($q, $log) { - function _obtainEffectiveMapId(d, mapId) { - var id, i; - if (!angular.isDefined(mapId)) { - if (Object.keys(d).length === 1) { - for (i in d) { - if (d.hasOwnProperty(i)) { - id = i; - } - } - } else if (Object.keys(d).length === 0) { - id = 'main'; - } else { - $log.error('[AngularJS - Leaflet] - You have more than 1 map on the DOM, you must provide the map ID to the leafletData.getXXX call'); - } - } else { - id = mapId; - } - return id; - } - function _getUnresolvedDefer(d, mapId) { - var id = _obtainEffectiveMapId(d, mapId), defer; - if (!angular.isDefined(d[id]) || d[id].resolvedDefer === true) { - defer = $q.defer(); - d[id] = { - defer: defer, - resolvedDefer: false }; - } else { - defer = d[id].defer; - } - return defer; + + leafletScope.$watchCollection('paths', function(newPaths) { + + // Delete paths (by name) from the array + for (var name in leafletPaths) { + if (!isDefined(newPaths[name])) { + map.removeLayer(leafletPaths[name]); + delete leafletPaths[name]; + } + } + + // Create the new paths + for (var newName in newPaths) { + if (newName.search('\\$') === 0) { + continue; + } + + if (newName.search('-') !== -1) { + $log.error('[AngularJS - Leaflet] The path name "' + newName + '" is not valid. It must not include "-" and a number.'); + continue; + } + + if (!isDefined(leafletPaths[newName])) { + var pathData = newPaths[newName]; + var newPath = createPath(newName, newPaths[newName], defaults); + + // bind popup if defined + if (isDefined(newPath) && isDefined(pathData.message)) { + newPath.bindPopup(pathData.message, pathData.popupOptions); + } + + // Show label if defined + if (leafletHelpers.LabelPlugin.isLoaded() && isDefined(pathData.label) && isDefined(pathData.label.message)) { + newPath.bindLabel(pathData.label.message, pathData.label.options); + } + + // Check if the marker should be added to a layer + if (isDefined(pathData) && isDefined(pathData.layer)) { + + if (!isString(pathData.layer)) { + $log.error('[AngularJS - Leaflet] A layername must be a string'); + continue; + } + + if (!isDefined(layers)) { + $log.error('[AngularJS - Leaflet] You must add layers to the directive if the markers are going to use this functionality.'); + continue; + } + + if (!isDefined(layers.overlays) || !isDefined(layers.overlays[pathData.layer])) { + $log.error('[AngularJS - Leaflet] A path can only be added to a layer of type "group"'); + continue; + } + + var layerGroup = layers.overlays[pathData.layer]; + if (!(layerGroup instanceof L.LayerGroup || layerGroup instanceof L.FeatureGroup)) { + $log.error('[AngularJS - Leaflet] Adding a path to an overlay needs a overlay of the type "group" or "featureGroup"'); + continue; + } + + // Listen for changes on the new path + leafletPaths[newName] = newPath; + + // The path goes to a correct layer group, so first of all we add it + layerGroup.addLayer(newPath); + + if (shouldWatch) { + watchPathFn(newPath, newName); + } else { + setPathOptions(newPath, pathData.type, pathData); + } + } else if (isDefined(newPath)) { + // Listen for changes on the new path + leafletPaths[newName] = newPath; + map.addLayer(newPath); + + if (shouldWatch) { + watchPathFn(newPath, newName); + } else { + setPathOptions(newPath, pathData.type, pathData); + } + } + + bindPathEvents(attrs.id, newPath, newName, pathData, leafletScope); + } + } + }); + }); + }); + }, + }; +}]); + +angular.module('leaflet-directive').directive('tiles', ["$log", "leafletData", "leafletMapDefaults", "leafletHelpers", function($log, leafletData, leafletMapDefaults, leafletHelpers) { + + return { + restrict: 'A', + scope: false, + replace: false, + require: 'leaflet', + + link: function(scope, element, attrs, controller) { + var isDefined = leafletHelpers.isDefined; + var leafletScope = controller.getLeafletScope(); + var tiles = leafletScope.tiles; + + if (!isDefined(tiles) || !isDefined(tiles.url)) { + $log.warn('[AngularJS - Leaflet] The \'tiles\' definition doesn\'t have the \'url\' property.'); + return; } - return { - isEmpty: function (value) { - return Object.keys(value).length === 0; - }, - isUndefinedOrEmpty: function (value) { - return angular.isUndefined(value) || value === null || Object.keys(value).length === 0; - }, - isDefined: function (value) { - return angular.isDefined(value) && value !== null; - }, - isNumber: function (value) { - return angular.isNumber(value); - }, - isString: function (value) { - return angular.isString(value); - }, - isArray: function (value) { - return angular.isArray(value); - }, - isObject: function (value) { - return angular.isObject(value); - }, - isFunction: function (value) { - return angular.isFunction(value); - }, - equals: function (o1, o2) { - return angular.equals(o1, o2); - }, - isValidCenter: function (center) { - return angular.isDefined(center) && angular.isNumber(center.lat) && angular.isNumber(center.lng) && angular.isNumber(center.zoom); - }, - isValidPoint: function (point) { - return angular.isDefined(point) && angular.isNumber(point.lat) && angular.isNumber(point.lng); - }, - isSameCenterOnMap: function (centerModel, map) { - var mapCenter = map.getCenter(); - var zoom = map.getZoom(); - if (mapCenter.lat === centerModel.lat && mapCenter.lng === centerModel.lng && zoom === centerModel.zoom) { - return true; + + controller.getMap().then(function(map) { + var defaults = leafletMapDefaults.getDefaults(attrs.id); + var tileLayerObj; + leafletScope.$watch('tiles', function(tiles, oldtiles) { + var tileLayerOptions = defaults.tileLayerOptions; + var tileLayerUrl = defaults.tileLayer; + + // If no valid tiles are in the scope, remove the last layer + if (!isDefined(tiles.url) && isDefined(tileLayerObj)) { + map.removeLayer(tileLayerObj); + return; } - return false; - }, - safeApply: function ($scope, fn) { - var phase = $scope.$root.$$phase; - if (phase === '$apply' || phase === '$digest') { - $scope.$eval(fn); - } else { - $scope.$apply(fn); - } - }, - obtainEffectiveMapId: _obtainEffectiveMapId, - getDefer: function (d, mapId) { - var id = _obtainEffectiveMapId(d, mapId), defer; - if (!angular.isDefined(d[id]) || d[id].resolvedDefer === false) { - defer = _getUnresolvedDefer(d, mapId); - } else { - defer = d[id].defer; - } - return defer; - }, - getUnresolvedDefer: _getUnresolvedDefer, - setResolvedDefer: function (d, mapId) { - var id = _obtainEffectiveMapId(d, mapId); - d[id].resolvedDefer = true; - }, - AwesomeMarkersPlugin: { - isLoaded: function () { - if (angular.isDefined(L.AwesomeMarkers) && angular.isDefined(L.AwesomeMarkers.Icon)) { - return true; + + // No leafletTiles object defined yet + if (!isDefined(tileLayerObj)) { + if (isDefined(tiles.options)) { + angular.copy(tiles.options, tileLayerOptions); + } + + if (isDefined(tiles.url)) { + tileLayerUrl = tiles.url; + } + + if (tiles.type === 'wms') { + tileLayerObj = L.tileLayer.wms(tileLayerUrl, tileLayerOptions); } else { - return false; + tileLayerObj = L.tileLayer(tileLayerUrl, tileLayerOptions); } - }, - is: function (icon) { - if (this.isLoaded()) { - return icon instanceof L.AwesomeMarkers.Icon; + + tileLayerObj.addTo(map); + leafletData.setTiles(tileLayerObj, attrs.id); + return; + } + + // If the options of the tilelayer is changed, we need to redraw the layer + if (isDefined(tiles.url) && isDefined(tiles.options) && + (tiles.type !== oldtiles.type || !angular.equals(tiles.options, tileLayerOptions))) { + map.removeLayer(tileLayerObj); + tileLayerOptions = defaults.tileLayerOptions; + angular.copy(tiles.options, tileLayerOptions); + tileLayerUrl = tiles.url; + + if (tiles.type === 'wms') { + tileLayerObj = L.tileLayer.wms(tileLayerUrl, tileLayerOptions); } else { - return false; - } - }, - equal: function (iconA, iconB) { - if (!this.isLoaded()) { - return false; - } - if (this.is(iconA)) { - return angular.equals(iconA, iconB); - } else { - return false; + tileLayerObj = L.tileLayer(tileLayerUrl, tileLayerOptions); } + + tileLayerObj.addTo(map); + leafletData.setTiles(tileLayerObj, attrs.id); + return; } - }, - LabelPlugin: { - isLoaded: function () { - return angular.isDefined(L.Label); - }, - is: function (layer) { - if (this.isLoaded()) { - return layer instanceof L.MarkerClusterGroup; - } else { - return false; - } + + // Only the URL of the layer is changed, update the tiles object + if (isDefined(tiles.url)) { + tileLayerObj.setUrl(tiles.url); } - }, - MarkerClusterPlugin: { - isLoaded: function () { - return angular.isDefined(L.MarkerClusterGroup); - }, - is: function (layer) { - if (this.isLoaded()) { - return layer instanceof L.MarkerClusterGroup; - } else { - return false; - } - } - }, - GoogleLayerPlugin: { - isLoaded: function () { - return angular.isDefined(L.Google); - }, - is: function (layer) { - if (this.isLoaded()) { - return layer instanceof L.Google; - } else { - return false; - } - } - }, - ChinaLayerPlugin: { - isLoaded: function () { - return angular.isDefined(L.tileLayer.chinaProvider); - } - }, - HeatMapLayerPlugin: { - isLoaded: function () { - return angular.isDefined(L.TileLayer.WebGLHeatMap); - } - }, - BingLayerPlugin: { - isLoaded: function () { - return angular.isDefined(L.BingLayer); - }, - is: function (layer) { - if (this.isLoaded()) { - return layer instanceof L.BingLayer; - } else { - return false; - } - } - }, - WFSLayerPlugin: { - isLoaded: function () { - return L.GeoJSON.WFS !== undefined; - }, - is: function (layer) { - if (this.isLoaded()) { - return layer instanceof L.GeoJSON.WFS; - } else { - return false; - } - } - }, - AGSLayerPlugin: { - isLoaded: function () { - return lvector !== undefined && lvector.AGS !== undefined; - }, - is: function (layer) { - if (this.isLoaded()) { - return layer instanceof lvector.AGS; - } else { - return false; - } - } - }, - YandexLayerPlugin: { - isLoaded: function () { - return angular.isDefined(L.Yandex); - }, - is: function (layer) { - if (this.isLoaded()) { - return layer instanceof L.Yandex; - } else { - return false; - } - } - }, - DynamicMapLayerPlugin: { - isLoaded: function () { - return L.esri !== undefined && L.esri.dynamicMapLayer !== undefined; - }, - is: function (layer) { - if (this.isLoaded()) { - return layer instanceof L.esri.dynamicMapLayer; - } else { - return false; - } - } - }, - GeoJSONPlugin: { - isLoaded: function () { - return angular.isDefined(L.TileLayer.GeoJSON); - }, - is: function (layer) { - if (this.isLoaded()) { - return layer instanceof L.TileLayer.GeoJSON; - } else { - return false; - } - } - }, - Leaflet: { - DivIcon: { - is: function (icon) { - return icon instanceof L.DivIcon; + }, true); + }); + }, + }; +}]); + +/* + Create multiple similar directives for watchOptions to support directiveControl + instead. (when watches are disabled) + NgAnnotate does not work here due to the functional creation +*/ +['markers', 'geojson'].forEach(function(name) { + angular.module('leaflet-directive').directive(name + 'WatchOptions', [ + '$log', '$rootScope', '$q', 'leafletData', 'leafletHelpers', + function($log, $rootScope, $q, leafletData, leafletHelpers) { + + var isDefined = leafletHelpers.isDefined, + errorHeader = leafletHelpers.errorHeader, + isObject = leafletHelpers.isObject, + _watchOptions = leafletHelpers.watchOptions; + + return { + restrict: 'A', + scope: false, + replace: false, + require: ['leaflet'], + + link: function(scope, element, attrs, controller) { + var mapController = controller[0], + leafletScope = mapController.getLeafletScope(); + + mapController.getMap().then(function() { + if (isDefined(scope[name + 'WatchOptions'])) { + if (isObject(scope[name + 'WatchOptions'])) + angular.extend(_watchOptions, scope[name + 'WatchOptions']); + else + $log.error(errorHeader + '[' + name + 'WatchOptions] is not an object'); + leafletScope[name + 'WatchOptions'] = _watchOptions; + } + }); }, - equal: function (iconA, iconB) { - if (this.is(iconA)) { - return angular.equals(iconA, iconB); + }; + },]); +}); + +angular.module('leaflet-directive') +.factory('LeafletEventsHelpersFactory', ["$rootScope", "$q", "$log", "leafletHelpers", function($rootScope, $q, $log, leafletHelpers) { + var safeApply = leafletHelpers.safeApply; + var isDefined = leafletHelpers.isDefined; + var isObject = leafletHelpers.isObject; + var isArray = leafletHelpers.isArray; + var errorHeader = leafletHelpers.errorHeader; + + var EventsHelper = function(rootBroadcastName, lObjectType) { + this.rootBroadcastName = rootBroadcastName; + $log.debug('LeafletEventsHelpersFactory: lObjectType: ' + lObjectType + 'rootBroadcastName: ' + rootBroadcastName); + + //used to path/key out certain properties based on the type , "markers", "geojson" + this.lObjectType = lObjectType; + }; + + EventsHelper.prototype.getAvailableEvents = function() {return [];}; + + /* + argument: name: Note this can be a single string or dot notation + Example: + markerModel : { + m1: { lat:_, lon: _} + } + //would yield name of + name = "m1" + + If nested: + markerModel : { + cars: { + m1: { lat:_, lon: _} + } + } + //would yield name of + name = "cars.m1" + */ + EventsHelper.prototype.genDispatchEvent = function(maybeMapId, eventName, logic, leafletScope, lObject, name, model, layerName, extra) { + var _this = this; + + maybeMapId = maybeMapId || ''; + if (maybeMapId) + maybeMapId = '.' + maybeMapId; + + return function(e) { + var broadcastName = _this.rootBroadcastName + maybeMapId + '.' + eventName; + $log.debug(broadcastName); + _this.fire(leafletScope, broadcastName, logic, e, e.target || lObject, model, name, layerName, extra); + }; + }; + + EventsHelper.prototype.fire = function(scope, broadcastName, logic, event, lObject, model, modelName, layerName) { + // Safely broadcast the event + safeApply(scope, function() { + var toSend = { + leafletEvent: event, + leafletObject: lObject, + modelName: modelName, + model: model, + }; + if (isDefined(layerName)) + angular.extend(toSend, {layerName: layerName}); + + if (logic === 'emit') { + scope.$emit(broadcastName, toSend); + } else { + $rootScope.$broadcast(broadcastName, toSend); + } + }); + }; + + EventsHelper.prototype.bindEvents = function(maybeMapId, lObject, name, model, leafletScope, layerName, extra) { + var events = []; + var logic = 'emit'; + var _this = this; + + if (!isDefined(leafletScope.eventBroadcast)) { + // Backward compatibility, if no event-broadcast attribute, all events are broadcasted + events = this.getAvailableEvents(); + } else if (!isObject(leafletScope.eventBroadcast)) { + // Not a valid object + $log.error(errorHeader + 'event-broadcast must be an object check your model.'); + } else { + // We have a possible valid object + if (!isDefined(leafletScope.eventBroadcast[_this.lObjectType])) { + // We do not have events enable/disable do we do nothing (all enabled by default) + events = this.getAvailableEvents(); + } else if (!isObject(leafletScope.eventBroadcast[_this.lObjectType])) { + // Not a valid object + $log.warn(errorHeader + 'event-broadcast.' + [_this.lObjectType] + ' must be an object check your model.'); + } else { + // We have a possible valid map object + // Event propadation logic + if (isDefined(leafletScope.eventBroadcast[this.lObjectType].logic)) { + // We take care of possible propagation logic + if (leafletScope.eventBroadcast[_this.lObjectType].logic !== 'emit' && + leafletScope.eventBroadcast[_this.lObjectType].logic !== 'broadcast') + $log.warn(errorHeader + 'Available event propagation logic are: \'emit\' or \'broadcast\'.'); + } + + // Enable / Disable + var eventsEnable = false; + var eventsDisable = false; + if (isDefined(leafletScope.eventBroadcast[_this.lObjectType].enable) && + isArray(leafletScope.eventBroadcast[_this.lObjectType].enable)) + eventsEnable = true; + if (isDefined(leafletScope.eventBroadcast[_this.lObjectType].disable) && + isArray(leafletScope.eventBroadcast[_this.lObjectType].disable)) + eventsDisable = true; + + if (eventsEnable && eventsDisable) { + // Both are active, this is an error + $log.warn(errorHeader + 'can not enable and disable events at the same time'); + } else if (!eventsEnable && !eventsDisable) { + // Both are inactive, this is an error + $log.warn(errorHeader + 'must enable or disable events'); + } else { + // At this point the object is OK, lets enable or disable events + if (eventsEnable) { + // Enable events + leafletScope.eventBroadcast[this.lObjectType].enable.forEach(function(eventName) { + // Do we have already the event enabled? + if (events.indexOf(eventName) !== -1) { + // Repeated event, this is an error + $log.warn(errorHeader + 'This event ' + eventName + ' is already enabled'); } else { - return false; + // Does the event exists? + if (_this.getAvailableEvents().indexOf(eventName) === -1) { + // The event does not exists, this is an error + $log.warn(errorHeader + 'This event ' + eventName + ' does not exist'); + } else { + // All ok enable the event + events.push(eventName); + } + } + }); + } else { + // Disable events + events = this.getAvailableEvents(); + leafletScope.eventBroadcast[_this.lObjectType].disable.forEach(function(eventName) { + var index = events.indexOf(eventName); + if (index === -1) { + // The event does not exist + $log.warn(errorHeader + 'This event ' + eventName + ' does not exist or has been already disabled'); + + } else { + events.splice(index, 1); + } + }); + } + } + } + } + + events.forEach(function(eventName) { + lObject.on(eventName, _this.genDispatchEvent(maybeMapId, eventName, logic, leafletScope, lObject, name, model, layerName, extra)); + }); + + return logic; + }; + + return EventsHelper; +}]) +.service('leafletEventsHelpers', ["LeafletEventsHelpersFactory", function(LeafletEventsHelpersFactory) { + return new LeafletEventsHelpersFactory(); +}]); + +angular.module('leaflet-directive') +.factory('leafletGeoJsonEvents', ["$rootScope", "$q", "$log", "leafletHelpers", "LeafletEventsHelpersFactory", "leafletData", function($rootScope, $q, $log, leafletHelpers, + LeafletEventsHelpersFactory, leafletData) { + var safeApply = leafletHelpers.safeApply; + var EventsHelper = LeafletEventsHelpersFactory; + + var GeoJsonEvents = function() { + EventsHelper.call(this, 'leafletDirectiveGeoJson', 'geojson'); + }; + + GeoJsonEvents.prototype = new EventsHelper(); + + GeoJsonEvents.prototype.genDispatchEvent = function(maybeMapId, eventName, logic, leafletScope, lObject, name, model, layerName, extra) { + var base = EventsHelper.prototype.genDispatchEvent.call(this, maybeMapId, eventName, logic, leafletScope, lObject, name, model, layerName); + var _this = this; + + return function(e) { + if (eventName === 'mouseout') { + if (extra.resetStyleOnMouseout) { + leafletData.getGeoJSON(extra.mapId) + .then(function(leafletGeoJSON) { + //this is broken on nested needs to traverse or user layerName (nested) + var lobj = layerName ? leafletGeoJSON[layerName] : leafletGeoJSON; + lobj.resetStyle(e.target); + }); + + } + + safeApply(leafletScope, function() { + $rootScope.$broadcast(_this.rootBroadcastName + '.mouseout', e); + }); + } + + base(e); //common + }; + }; + + GeoJsonEvents.prototype.getAvailableEvents = function() { return [ + 'click', + 'dblclick', + 'mouseover', + 'mouseout', + ]; + }; + + return new GeoJsonEvents(); +}]); + +angular.module('leaflet-directive') +.factory('leafletLabelEvents', ["$rootScope", "$q", "$log", "leafletHelpers", "LeafletEventsHelpersFactory", function($rootScope, $q, $log, leafletHelpers, LeafletEventsHelpersFactory) { + var Helpers = leafletHelpers; + var EventsHelper = LeafletEventsHelpersFactory; + + var LabelEvents = function() { + EventsHelper.call(this, 'leafletDirectiveLabel', 'markers'); + }; + + LabelEvents.prototype = new EventsHelper(); + + LabelEvents.prototype.genDispatchEvent = function(maybeMapId, eventName, logic, leafletScope, lObject, name, model, layerName) { + var markerName = name.replace('markers.', ''); + return EventsHelper.prototype + .genDispatchEvent.call(this, maybeMapId, eventName, logic, leafletScope, lObject, markerName, model, layerName); + }; + + LabelEvents.prototype.getAvailableEvents = function() { + return [ + 'click', + 'dblclick', + 'mousedown', + 'mouseover', + 'mouseout', + 'contextmenu', + ]; + }; + + LabelEvents.prototype.genEvents = function(maybeMapId, eventName, logic, leafletScope, lObject, name, model, layerName) { + var _this = this; + var labelEvents = this.getAvailableEvents(); + var scopeWatchName = Helpers.getObjectArrayPath('markers.' + name); + labelEvents.forEach(function(eventName) { + lObject.label.on(eventName, _this.genDispatchEvent( + maybeMapId, eventName, logic, leafletScope, lObject.label, scopeWatchName, model, layerName)); + }); + }; + + LabelEvents.prototype.bindEvents = function() {}; + + return new LabelEvents(); +}]); + +angular.module('leaflet-directive') +.factory('leafletMapEvents', ["$rootScope", "$q", "$log", "leafletHelpers", "leafletEventsHelpers", "leafletIterators", function($rootScope, $q, $log, leafletHelpers, leafletEventsHelpers, leafletIterators) { + var isDefined = leafletHelpers.isDefined; + var fire = leafletEventsHelpers.fire; + + var _getAvailableMapEvents = function() { + return [ + 'click', + 'dblclick', + 'mousedown', + 'mouseup', + 'mouseover', + 'mouseout', + 'mousemove', + 'contextmenu', + 'focus', + 'blur', + 'preclick', + 'load', + 'unload', + 'viewreset', + 'movestart', + 'move', + 'moveend', + 'dragstart', + 'drag', + 'dragend', + 'zoomstart', + 'zoomanim', + 'zoomend', + 'zoomlevelschange', + 'resize', + 'autopanstart', + 'layeradd', + 'layerremove', + 'baselayerchange', + 'overlayadd', + 'overlayremove', + 'locationfound', + 'locationerror', + 'popupopen', + 'popupclose', + 'draw:created', + 'draw:edited', + 'draw:deleted', + 'draw:drawstart', + 'draw:drawstop', + 'draw:editstart', + 'draw:editstop', + 'draw:deletestart', + 'draw:deletestop', + ]; + }; + + var _genDispatchMapEvent = function(scope, eventName, logic, maybeMapId) { + if (maybeMapId) + maybeMapId = maybeMapId + '.'; + return function(e) { + // Put together broadcast name + var broadcastName = 'leafletDirectiveMap.' + maybeMapId + eventName; + $log.debug(broadcastName); + + // Safely broadcast the event + fire(scope, broadcastName, logic, e, e.target, scope); + }; + }; + + var _notifyCenterChangedToBounds = function(scope) { + scope.$broadcast('boundsChanged'); + }; + + var _notifyCenterUrlHashChanged = function(scope, map, attrs, search) { + if (!isDefined(attrs.urlHashCenter)) { + return; + } + + var center = map.getCenter(); + var centerUrlHash = (center.lat).toFixed(4) + ':' + (center.lng).toFixed(4) + ':' + map.getZoom(); + if (!isDefined(search.c) || search.c !== centerUrlHash) { + //$log.debug("notified new center..."); + scope.$emit('centerUrlHash', centerUrlHash); + } + }; + + var _addEvents = function(map, mapEvents, contextName, scope, logic) { + leafletIterators.each(mapEvents, function(eventName) { + var context = {}; + context[contextName] = eventName; + map.on(eventName, _genDispatchMapEvent(scope, eventName, logic, map._container.id || ''), context); + }); + }; + + return { + getAvailableMapEvents: _getAvailableMapEvents, + genDispatchMapEvent: _genDispatchMapEvent, + notifyCenterChangedToBounds: _notifyCenterChangedToBounds, + notifyCenterUrlHashChanged: _notifyCenterUrlHashChanged, + addEvents: _addEvents, + }; +}]); + +angular.module('leaflet-directive') +.factory('leafletMarkerEvents', ["$rootScope", "$q", "$log", "leafletHelpers", "LeafletEventsHelpersFactory", "leafletLabelEvents", function($rootScope, $q, $log, leafletHelpers, LeafletEventsHelpersFactory, leafletLabelEvents) { + var safeApply = leafletHelpers.safeApply; + var isDefined = leafletHelpers.isDefined; + var Helpers = leafletHelpers; + var lblHelp = leafletLabelEvents; + var EventsHelper = LeafletEventsHelpersFactory; + + var MarkerEvents = function() { + EventsHelper.call(this, 'leafletDirectiveMarker', 'markers'); + }; + + MarkerEvents.prototype = new EventsHelper(); + + MarkerEvents.prototype.genDispatchEvent = function(maybeMapId, eventName, logic, leafletScope, lObject, name, model, layerName) { + var handle = EventsHelper.prototype + .genDispatchEvent.call(this, maybeMapId, eventName, logic, leafletScope, lObject, name, model, layerName); + return function(e) { + // Broadcast old marker click name for backwards compatibility + if (eventName === 'click') { + safeApply(leafletScope, function() { + $rootScope.$broadcast('leafletDirectiveMarkersClick', name); + }); + } else if (eventName === 'dragend') { + safeApply(leafletScope, function() { + model.lat = lObject.getLatLng().lat; + model.lng = lObject.getLatLng().lng; + }); + + if (model.message && model.focus === true) { + lObject.openPopup(); + } + } + + handle(e); //common + }; + }; + + MarkerEvents.prototype.getAvailableEvents = function() { return [ + 'click', + 'dblclick', + 'mousedown', + 'mouseover', + 'mouseout', + 'contextmenu', + 'dragstart', + 'drag', + 'dragend', + 'move', + 'remove', + 'popupopen', + 'popupclose', + 'touchend', + 'touchstart', + 'touchmove', + 'touchcancel', + 'touchleave', + ]; + }; + + MarkerEvents.prototype.bindEvents = function(maybeMapId, lObject, name, model, leafletScope, layerName) { + var logic = EventsHelper.prototype.bindEvents.call(this, maybeMapId, lObject, name, model, leafletScope, layerName); + + if (Helpers.LabelPlugin.isLoaded() && isDefined(lObject.label)) { + lblHelp.genEvents(maybeMapId, name, logic, leafletScope, lObject, model, layerName); + } + }; + + return new MarkerEvents(); +}]); + +angular.module('leaflet-directive') +.factory('leafletPathEvents', ["$rootScope", "$q", "$log", "leafletHelpers", "leafletLabelEvents", "leafletEventsHelpers", function($rootScope, $q, $log, leafletHelpers, leafletLabelEvents, leafletEventsHelpers) { + var isDefined = leafletHelpers.isDefined; + var isObject = leafletHelpers.isObject; + var Helpers = leafletHelpers; + var errorHeader = leafletHelpers.errorHeader; + var lblHelp = leafletLabelEvents; + var fire = leafletEventsHelpers.fire; + + /* + TODO (nmccready) This EventsHelper needs to be derrived from leafletEventsHelpers to elminate copy and paste code. + */ + + var _genDispatchPathEvent = function(maybeMapId, eventName, logic, leafletScope, lObject, name, model, layerName) { + maybeMapId = maybeMapId || ''; + + if (maybeMapId) + maybeMapId = '.' + maybeMapId; + + return function(e) { + var broadcastName = 'leafletDirectivePath' + maybeMapId + '.' + eventName; + $log.debug(broadcastName); + fire(leafletScope, broadcastName, logic, e, e.target || lObject, model, name, layerName); + }; + }; + + var _bindPathEvents = function(maybeMapId, lObject, name, model, leafletScope) { + var pathEvents = []; + var i; + var eventName; + var logic = 'broadcast'; + + if (!isDefined(leafletScope.eventBroadcast)) { + // Backward compatibility, if no event-broadcast attribute, all events are broadcasted + pathEvents = _getAvailablePathEvents(); + } else if (!isObject(leafletScope.eventBroadcast)) { + // Not a valid object + $log.error(errorHeader + 'event-broadcast must be an object check your model.'); + } else { + // We have a possible valid object + if (!isDefined(leafletScope.eventBroadcast.path)) { + // We do not have events enable/disable do we do nothing (all enabled by default) + pathEvents = _getAvailablePathEvents(); + } else if (isObject(leafletScope.eventBroadcast.paths)) { + // Not a valid object + $log.warn(errorHeader + 'event-broadcast.path must be an object check your model.'); + } else { + // We have a possible valid map object + // Event propadation logic + if (leafletScope.eventBroadcast.path.logic !== undefined && leafletScope.eventBroadcast.path.logic !== null) { + // We take care of possible propagation logic + if (leafletScope.eventBroadcast.path.logic !== 'emit' && leafletScope.eventBroadcast.path.logic !== 'broadcast') { + // This is an error + $log.warn(errorHeader + 'Available event propagation logic are: \'emit\' or \'broadcast\'.'); + } else if (leafletScope.eventBroadcast.path.logic === 'emit') { + logic = 'emit'; + } + } + + // Enable / Disable + var pathEventsEnable = false; + var pathEventsDisable = false; + if (leafletScope.eventBroadcast.path.enable !== undefined && leafletScope.eventBroadcast.path.enable !== null) { + if (typeof leafletScope.eventBroadcast.path.enable === 'object') { + pathEventsEnable = true; + } + } + + if (leafletScope.eventBroadcast.path.disable !== undefined && leafletScope.eventBroadcast.path.disable !== null) { + if (typeof leafletScope.eventBroadcast.path.disable === 'object') { + pathEventsDisable = true; + } + } + + if (pathEventsEnable && pathEventsDisable) { + // Both are active, this is an error + $log.warn(errorHeader + 'can not enable and disable events at the same time'); + } else if (!pathEventsEnable && !pathEventsDisable) { + // Both are inactive, this is an error + $log.warn(errorHeader + 'must enable or disable events'); + } else { + // At this point the path object is OK, lets enable or disable events + if (pathEventsEnable) { + // Enable events + for (i = 0; i < leafletScope.eventBroadcast.path.enable.length; i++) { + eventName = leafletScope.eventBroadcast.path.enable[i]; + + // Do we have already the event enabled? + if (pathEvents.indexOf(eventName) !== -1) { + // Repeated event, this is an error + $log.warn(errorHeader + 'This event ' + eventName + ' is already enabled'); + } else { + // Does the event exists? + if (_getAvailablePathEvents().indexOf(eventName) === -1) { + // The event does not exists, this is an error + $log.warn(errorHeader + 'This event ' + eventName + ' does not exist'); + } else { + // All ok enable the event + pathEvents.push(eventName); + } } } - }, - Icon: { - is: function (icon) { - return icon instanceof L.Icon; - }, - equal: function (iconA, iconB) { - if (this.is(iconA)) { - return angular.equals(iconA, iconB); + } else { + // Disable events + pathEvents = _getAvailablePathEvents(); + for (i = 0; i < leafletScope.eventBroadcast.path.disable.length; i++) { + eventName = leafletScope.eventBroadcast.path.disable[i]; + var index = pathEvents.indexOf(eventName); + if (index === -1) { + // The event does not exist + $log.warn(errorHeader + 'This event ' + eventName + ' does not exist or has been already disabled'); + } else { - return false; + pathEvents.splice(index, 1); } } } } - }; + } } - ]); -}()); \ No newline at end of file + + for (i = 0; i < pathEvents.length; i++) { + eventName = pathEvents[i]; + lObject.on(eventName, _genDispatchPathEvent(maybeMapId, eventName, logic, leafletScope, pathEvents, name)); + } + + if (Helpers.LabelPlugin.isLoaded() && isDefined(lObject.label)) { + lblHelp.genEvents(maybeMapId, name, logic, leafletScope, lObject, model); + } + }; + + var _getAvailablePathEvents = function() { + return [ + 'click', + 'dblclick', + 'mousedown', + 'mouseover', + 'mouseout', + 'contextmenu', + 'add', + 'remove', + 'popupopen', + 'popupclose', + ]; + }; + + return { + getAvailablePathEvents: _getAvailablePathEvents, + bindPathEvents: _bindPathEvents, + }; +}]); + +}(angular)); \ No newline at end of file diff --git a/app/bower_components/angular-leaflet-directive/dist/angular-leaflet-directive.min.js b/app/bower_components/angular-leaflet-directive/dist/angular-leaflet-directive.min.js index 8a14f23..cbcd840 100644 --- a/app/bower_components/angular-leaflet-directive/dist/angular-leaflet-directive.min.js +++ b/app/bower_components/angular-leaflet-directive/dist/angular-leaflet-directive.min.js @@ -27,6 +27,14 @@ * @authors https://github.com/tombatossals/angular-leaflet-directive/graphs/contributors */ -/*! angular-leaflet-directive 06-05-2014 */ -!function(){"use strict";angular.module("leaflet-directive",[]).directive("leaflet",["$q","leafletData","leafletMapDefaults","leafletHelpers","leafletEvents",function(a,b,c,d,e){var f;return{restrict:"EA",replace:!0,scope:{center:"=center",defaults:"=defaults",maxbounds:"=maxbounds",bounds:"=bounds",markers:"=markers",legend:"=legend",geojson:"=geojson",paths:"=paths",tiles:"=tiles",layers:"=layers",controls:"=controls",eventBroadcast:"=eventBroadcast"},template:'
        ',controller:["$scope",function(b){f=a.defer(),this.getMap=function(){return f.promise},this.getLeafletScope=function(){return b}}],link:function(a,g,h){var i=d.isDefined,j=c.setDefaults(a.defaults,h.id),k=e.genDispatchMapEvent,l=e.getAvailableMapEvents();i(h.width)&&(isNaN(h.width)?g.css("width",h.width):g.css("width",h.width+"px")),i(h.height)&&(isNaN(h.height)?g.css("height",h.height):g.css("height",h.height+"px"));var m=new L.Map(g[0],c.getMapCreationDefaults(h.id));if(f.resolve(m),i(h.center)||m.setView([j.center.lat,j.center.lng],j.center.zoom),!i(h.tiles)&&!i(h.layers)){var n=L.tileLayer(j.tileLayer,j.tileLayerOptions);n.addTo(m),b.setTiles(n,h.id)}if(i(m.zoomControl)&&i(j.zoomControlPosition)&&m.zoomControl.setPosition(j.zoomControlPosition),i(m.zoomControl)&&j.zoomControl===!1&&m.zoomControl.removeFrom(m),i(m.zoomsliderControl)&&i(j.zoomsliderControl)&&j.zoomsliderControl===!1&&m.zoomsliderControl.removeFrom(m),!i(h.eventBroadcast))for(var o="broadcast",p=0;pe.center.zoom?{setView:!0,maxZoom:c.zoom}:i(e.maxZoom)?{setView:!0,maxZoom:e.maxZoom}:{setView:!0})):void(s&&k(c,b)||(b.setView([c.lat,c.lng],c.zoom),g.notifyCenterChangedToBounds(q,b))):void a.warn("[AngularJS - Leaflet] invalid 'center'")},!0),b.whenReady(function(){s=!0}),b.on("moveend",function(){h.resolve(),g.notifyCenterUrlHashChanged(q,b,n,c.search()),k(r,b)||l(q,function(a){a.center={lat:b.getCenter().lat,lng:b.getCenter().lng,zoom:b.getZoom(),autoDiscover:!1},g.notifyCenterChangedToBounds(q,b)})}),r.autoDiscover===!0&&b.on("locationerror",function(){a.warn("[AngularJS - Leaflet] The Geolocation API is unauthorized on this page."),m(r)?(b.setView([r.lat,r.lng],r.zoom),g.notifyCenterChangedToBounds(q,b)):(b.setView([e.center.lat,e.center.lng],e.center.zoom),g.notifyCenterChangedToBounds(q,b))})})}}}]),angular.module("leaflet-directive").directive("tiles",["$log","leafletData","leafletMapDefaults","leafletHelpers",function(a,b,c,d){return{restrict:"A",scope:!1,replace:!1,require:"leaflet",link:function(e,f,g,h){var i=d.isDefined,j=h.getLeafletScope(),k=j.tiles;return i(k)||i(k.url)?void h.getMap().then(function(a){var d,e=c.getDefaults(g.id);j.$watch("tiles",function(c){var f=e.tileLayerOptions,h=e.tileLayer;return!i(c.url)&&i(d)?void a.removeLayer(d):i(d)?i(c.url)&&i(c.options)&&!angular.equals(c.options,f)?(a.removeLayer(d),f=e.tileLayerOptions,angular.copy(c.options,f),h=c.url,d=L.tileLayer(h,f),d.addTo(a),void b.setTiles(d,g.id)):void(i(c.url)&&d.setUrl(c.url)):(i(c.options)&&angular.copy(c.options,f),i(c.url)&&(h=c.url),d=L.tileLayer(h,f),d.addTo(a),void b.setTiles(d,g.id))},!0)}):void a.warn("[AngularJS - Leaflet] The 'tiles' definition doesn't have the 'url' property.")}}}]),angular.module("leaflet-directive").directive("legend",["$log","$http","leafletHelpers","leafletLegendHelpers",function(a,b,c,d){return{restrict:"A",scope:!1,replace:!1,require:"leaflet",link:function(e,f,g,h){var i,j=c.isArray,k=c.isDefined,l=c.isFunction,m=h.getLeafletScope(),n=m.legend,o=n.legendClass?n.legendClass:"legend",p=n.position||"bottomright";h.getMap().then(function(c){k(n.url)||j(n.colors)&&j(n.labels)&&n.colors.length===n.labels.length?k(n.url)?a.info("[AngularJS - Leaflet] loading arcgis legend service."):(i=L.control({position:p}),i.onAdd=d.getOnAddArrayLegend(n,o),i.addTo(c)):a.warn("[AngularJS - Leaflet] legend.colors and legend.labels must be set."),m.$watch("legend.url",function(e){k(e)&&b.get(e).success(function(a){k(i)?d.updateArcGISLegend(i.getContainer(),a):(i=L.control({position:p}),i.onAdd=d.getOnAddArcGISLegend(a,o),i.addTo(c)),k(n.loadedData)&&l(n.loadedData)&&n.loadedData()}).error(function(){a.warn("[AngularJS - Leaflet] legend.url not loaded.")})})})}}}]),angular.module("leaflet-directive").directive("geojson",["$log","$rootScope","leafletData","leafletHelpers",function(a,b,c,d){return{restrict:"A",scope:!1,replace:!1,require:"leaflet",link:function(a,e,f,g){var h=d.safeApply,i=d.isDefined,j=g.getLeafletScope(),k={};g.getMap().then(function(a){j.$watch("geojson",function(e){if(i(k)&&a.hasLayer(k)&&a.removeLayer(k),i(e)&&i(e.data)){var f=e.resetStyleOnMouseout,g=e.onEachFeature;g||(g=function(a,c){d.LabelPlugin.isLoaded()&&i(e.label)&&c.bindLabel(a.properties.description),c.on({mouseover:function(c){h(j,function(){e.selected=a,b.$broadcast("leafletDirectiveMap.geojsonMouseover",c)})},mouseout:function(a){f&&k.resetStyle(a.target),h(j,function(){e.selected=void 0,b.$broadcast("leafletDirectiveMap.geojsonMouseout",a)})},click:function(c){h(j,function(){e.selected=a,b.$broadcast("leafletDirectiveMap.geojsonClick",e.selected,c)})}})}),e.options={style:e.style,filter:e.filter,onEachFeature:g,pointToLayer:e.pointToLayer},k=L.geoJson(e.data,e.options),c.setGeoJSON(k),k.addTo(a)}})})}}}]),angular.module("leaflet-directive").directive("layers",["$log","$q","leafletData","leafletHelpers","leafletLayerHelpers","leafletControlHelpers",function(a,b,c,d,e,f){var g;return{restrict:"A",scope:!1,replace:!1,require:"leaflet",controller:function(){g=b.defer(),this.getLayers=function(){return g.promise}},link:function(b,h,i,j){var k=d.isDefined,l={},m=j.getLeafletScope(),n=m.layers,o=e.createLayer,p=f.updateLayersControl,q=!1;j.getMap().then(function(b){if(!k(n)||!k(n.baselayers)||0===Object.keys(n.baselayers).length)return void a.error("[AngularJS - Leaflet] At least one baselayer has to be defined");g.resolve(l),c.setLayers(l,i.id),l.baselayers={},l.overlays={};var d=i.id,e=!1;for(var f in n.baselayers){var h=o(n.baselayers[f]);k(h)?(l.baselayers[f]=h,n.baselayers[f].top===!0&&(b.addLayer(l.baselayers[f]),e=!0)):delete n.baselayers[f]}!e&&Object.keys(l.baselayers).length>0&&b.addLayer(l.baselayers[Object.keys(n.baselayers)[0]]);for(f in n.overlays){var j=o(n.overlays[f]);k(j)?(l.overlays[f]=j,n.overlays[f].visible===!0&&b.addLayer(l.overlays[f])):delete n.overlays[f]}m.$watch("layers.baselayers",function(c){for(var e in l.baselayers)k(c[e])||(b.hasLayer(l.baselayers[e])&&b.removeLayer(l.baselayers[e]),delete l.baselayers[e]);for(var f in c)if(!k(l.baselayers[f])){var g=o(c[f]);k(g)&&(l.baselayers[f]=g,c[f].top===!0&&b.addLayer(l.baselayers[f]))}if(0===Object.keys(l.baselayers).length)return void a.error("[AngularJS - Leaflet] At least one baselayer has to be defined");var h=!1;for(var i in l.baselayers)if(b.hasLayer(l.baselayers[i])){h=!0;break}h||b.addLayer(l.baselayers[Object.keys(n.baselayers)[0]]),q=p(b,d,q,c,n.overlays,l)},!0),m.$watch("layers.overlays",function(a){for(var c in l.overlays)k(a[c])||(b.hasLayer(l.overlays[c])&&b.removeLayer(l.overlays[c]),delete l.overlays[c]);for(var e in a){if(!k(l.overlays[e])){var f=o(a[e]);k(f)&&(l.overlays[e]=f,a[e].visible===!0&&b.addLayer(l.overlays[e]))}a[e].visible&&!b.hasLayer(l.overlays[e])?b.addLayer(l.overlays[e]):a[e].visible===!1&&b.hasLayer(l.overlays[e])&&b.removeLayer(l.overlays[e])}q=p(b,d,q,n.baselayers,a,l)},!0)})}}}]),angular.module("leaflet-directive").directive("bounds",["$log","$timeout","leafletHelpers","leafletBoundsHelpers",function(a,b,c,d){return{restrict:"A",scope:!1,replace:!1,require:["leaflet","center"],link:function(b,e,f,g){var h=c.isDefined,i=d.createLeafletBounds,j=g[0].getLeafletScope(),k=g[0],l=function(a){return 0===a._southWest.lat&&0===a._southWest.lng&&0===a._northEast.lat&&0===a._northEast.lng?!0:!1};k.getMap().then(function(b){j.$on("boundsChanged",function(a){var c=a.currentScope,d=b.getBounds();if(!l(d)){var e={northEast:{lat:d._northEast.lat,lng:d._northEast.lng},southWest:{lat:d._southWest.lat,lng:d._southWest.lng}};angular.equals(c.bounds,e)||(c.bounds=e)}}),j.$watch("bounds",function(c){if(!h(c))return void a.error("[AngularJS - Leaflet] Invalid bounds");var d=i(c);d&&!b.getBounds().equals(d)&&b.fitBounds(d)},!0)})}}}]),angular.module("leaflet-directive").directive("markers",["$log","$rootScope","$q","leafletData","leafletHelpers","leafletMapDefaults","leafletMarkersHelpers","leafletEvents",function(a,b,c,d,e,f,g,h){return{restrict:"A",scope:!1,replace:!1,require:["leaflet","?layers"],link:function(b,f,i,j){var k=j[0],l=e,m=e.isDefined,n=e.isString,o=k.getLeafletScope(),p=o.markers,q=g.deleteMarker,r=g.addMarkerWatcher,s=g.listenMarkerEvents,t=g.addMarkerToGroup,u=h.bindMarkerEvents,v=g.createMarker;k.getMap().then(function(b){var e,f={};e=m(j[1])?j[1].getLayers:function(){var a=c.defer();return a.resolve(),a.promise},m(p)&&e().then(function(c){d.setMarkers(f,i.id),o.$watch("markers",function(d){for(var e in f)m(d)&&m(d[e])||(q(f[e],b,c),delete f[e]);for(var g in d)if(-1===g.search("-")){if(!m(f[g])){var h=d[g],j=v(h);if(!m(j)){a.error("[AngularJS - Leaflet] Received invalid data on the marker "+g+".");continue}if(f[g]=j,m(h.message)&&j.bindPopup(h.message,h.popupOptions),m(h.group)&&t(j,h.group,b),l.LabelPlugin.isLoaded()&&m(h.label)&&m(h.label.message)&&j.bindLabel(h.label.message,h.label.options),m(h)&&m(h.layer)){if(!n(h.layer)){a.error("[AngularJS - Leaflet] A layername must be a string");continue}if(!m(c)){a.error("[AngularJS - Leaflet] You must add layers to the directive if the markers are going to use this functionality.");continue}if(!m(c.overlays)||!m(c.overlays[h.layer])){a.error('[AngularJS - Leaflet] A marker can only be added to a layer of type "group"');continue}var k=c.overlays[h.layer];if(!(k instanceof L.LayerGroup)){a.error('[AngularJS - Leaflet] Adding a marker to an overlay needs a overlay of the type "group"');continue}k.addLayer(j),b.hasLayer(j)&&h.focus===!0&&j.openPopup()}else m(h.group)||(b.addLayer(j),h.focus===!0&&j.openPopup(),l.LabelPlugin.isLoaded()&&m(h.label)&&m(h.label.options)&&h.label.options.noHide===!0&&j.showLabel());var p=!m(i.watchMarkers)||"true"===i.watchMarkers;p&&(r(j,g,o,c,b),s(j,h,o)),u(j,g,h,o)}}else a.error('The marker can\'t use a "-" on his key name: "'+g+'".')},!0)})})}}}]),angular.module("leaflet-directive").directive("paths",["$log","leafletData","leafletMapDefaults","leafletHelpers","leafletPathsHelpers","leafletEvents",function(a,b,c,d,e,f){return{restrict:"A",scope:!1,replace:!1,require:"leaflet",link:function(g,h,i,j){var k=d.isDefined,l=j.getLeafletScope(),m=l.paths,n=e.createPath,o=f.bindPathEvents,p=e.setPathOptions;j.getMap().then(function(e){var f=c.getDefaults(i.id);if(k(m)){var g={};b.setPaths(g,i.id);var h=function(a,b){var c=l.$watch("paths."+b,function(b){return k(b)?void p(a,b.type,b):(e.removeLayer(a),void c())},!0)};l.$watch("paths",function(b){for(var c in b)if(-1===c.search("-")){if(!k(g[c])){var i=b[c],j=n(c,b[c],f);k(j)&&k(i.message)&&j.bindPopup(i.message),d.LabelPlugin.isLoaded()&&k(i.label)&&k(i.label.message)&&j.bindLabel(i.label.message,i.label.options),k(j)&&(g[c]=j,e.addLayer(j),h(j,c)),o(j,c,i,l)}}else a.error('[AngularJS - Leaflet] The path name "'+c+'" is not valid. It must not include "-" and a number.');for(var m in g)k(b[m])||delete g[m]},!0)}})}}}]),angular.module("leaflet-directive").directive("controls",["$log","leafletHelpers",function(a,b){return{restrict:"A",scope:!1,replace:!1,require:"?^leaflet",link:function(a,c,d,e){if(e){var f=b.isDefined,g=e.getLeafletScope(),h=g.controls;e.getMap().then(function(a){if(f(L.Control.Draw)&&f(h.draw)){var b=new L.FeatureGroup;a.addLayer(b);var c={edit:{featureGroup:b}};angular.extend(c,h.draw.options);var d=new L.Control.Draw(c);a.addControl(d)}if(f(h.custom))for(var e in h.custom)a.addControl(h.custom[e])})}}}}]),angular.module("leaflet-directive").directive("eventBroadcast",["$log","$rootScope","leafletHelpers","leafletEvents",function(a,b,c,d){return{restrict:"A",scope:!1,replace:!1,require:"leaflet",link:function(b,e,f,g){var h=c.isObject,i=g.getLeafletScope(),j=i.eventBroadcast,k=d.getAvailableMapEvents(),l=d.genDispatchMapEvent;g.getMap().then(function(b){var c,d,e=[],f="broadcast";if(h(j)){if(void 0===j.map||null===j.map)e=k;else if("object"!=typeof j.map)a.warn("[AngularJS - Leaflet] event-broadcast.map must be an object check your model.");else{void 0!==j.map.logic&&null!==j.map.logic&&("emit"!==j.map.logic&&"broadcast"!==j.map.logic?a.warn("[AngularJS - Leaflet] Available event propagation logic are: 'emit' or 'broadcast'."):"emit"===j.map.logic&&(f="emit"));var g=!1,m=!1;if(void 0!==j.map.enable&&null!==j.map.enable&&"object"==typeof j.map.enable&&(g=!0),void 0!==j.map.disable&&null!==j.map.disable&&"object"==typeof j.map.disable&&(m=!0),g&&m)a.warn("[AngularJS - Leaflet] can not enable and disable events at the time");else if(g||m)if(g)for(c=0;cOpenStreetMap contributors'},path:{weight:10,opacity:1,color:"#0000ff"},center:{lat:0,lng:0,zoom:1}}}var d=b.isDefined,e=b.obtainEffectiveMapId,f={};return{getDefaults:function(a){var b=e(f,a);return f[b]},getMapCreationDefaults:function(a){var b=e(f,a),c=f[b],g={maxZoom:c.maxZoom,keyboard:c.keyboard,dragging:c.dragging,zoomControl:c.zoomControl,doubleClickZoom:c.doubleClickZoom,scrollWheelZoom:c.scrollWheelZoom,attributionControl:c.attributionControl,worldCopyJump:c.worldCopyJump,crs:c.crs};if(d(c.minZoom)&&(g.minZoom=c.minZoom),d(c.zoomAnimation)&&(g.zoomAnimation=c.zoomAnimation),d(c.fadeAnimation)&&(g.fadeAnimation=c.fadeAnimation),d(c.markerZoomAnimation)&&(g.markerZoomAnimation=c.markerZoomAnimation),c.map)for(var h in c.map)g[h]=c.map[h];return g},setDefaults:function(a,b){var g=c();d(a)&&(g.doubleClickZoom=d(a.doubleClickZoom)?a.doubleClickZoom:g.doubleClickZoom,g.scrollWheelZoom=d(a.scrollWheelZoom)?a.scrollWheelZoom:g.doubleClickZoom,g.zoomControl=d(a.zoomControl)?a.zoomControl:g.zoomControl,g.zoomsliderControl=d(a.zoomsliderControl)?a.zoomsliderControl:g.zoomsliderControl,g.attributionControl=d(a.attributionControl)?a.attributionControl:g.attributionControl,g.tileLayer=d(a.tileLayer)?a.tileLayer:g.tileLayer,g.zoomControlPosition=d(a.zoomControlPosition)?a.zoomControlPosition:g.zoomControlPosition,g.keyboard=d(a.keyboard)?a.keyboard:g.keyboard,g.dragging=d(a.dragging)?a.dragging:g.dragging,d(a.controls)&&angular.extend(g.controls,a.controls),d(a.crs)&&d(L.CRS[a.crs])&&(g.crs=L.CRS[a.crs]),d(a.tileLayerOptions)&&angular.copy(a.tileLayerOptions,g.tileLayerOptions),d(a.maxZoom)&&(g.maxZoom=a.maxZoom),d(a.minZoom)&&(g.minZoom=a.minZoom),d(a.zoomAnimation)&&(g.zoomAnimation=a.zoomAnimation),d(a.fadeAnimation)&&(g.fadeAnimation=a.fadeAnimation),d(a.markerZoomAnimation)&&(g.markerZoomAnimation=a.markerZoomAnimation),d(a.worldCopyJump)&&(g.worldCopyJump=a.worldCopyJump),d(a.map)&&(g.map=a.map));var h=e(f,b);return f[h]=g,g}}}]),angular.module("leaflet-directive").factory("leafletEvents",["$rootScope","$q","$log","leafletHelpers",function(a,b,c,d){var e=d.safeApply,f=d.isDefined,g=d.isObject,h=d,i=function(){return["click","dblclick","mousedown","mouseover","mouseout","contextmenu"]},j=function(a,b,c,d){for(var e=i(),f="markers."+d,g=0;g1},i=function(a){var b,c=d.getDefaults(a),e={collapsed:c.controls.layers.collapsed,position:c.controls.layers.position};return b=c.controls.layers&&g(c.controls.layers.control)?c.controls.layers.control.apply(this,[[],[],e]):new L.control.layers([],[],e)};return{layersControlMustBeVisible:h,updateLayersControl:function(a,b,c,d,f,j){var k,l=h(d,f);if(g(e)&&c){for(k in j.baselayers)e.removeLayer(j.baselayers[k]);for(k in j.overlays)e.removeLayer(j.overlays[k]);e.removeFrom(a)}if(l){e=i(b);for(k in d)g(j.baselayers[k])&&e.addBaseLayer(j.baselayers[k],d[k].name);for(k in f)g(j.overlays[k])&&e.addOverlay(j.overlays[k],f[k].name);e.addTo(a)}return l}}}]),angular.module("leaflet-directive").factory("leafletLegendHelpers",function(){var a=function(a,b){if(a.innerHTML="",b.error)a.innerHTML+='
        '+b.error.message+"
        ";else for(var c=0;c'+d.layerName+"
        ";for(var e=0;e
        '+f.label+"
        "}}},b=function(b,c){return function(){var d=L.DomUtil.create("div",c);return L.Browser.touch?L.DomEvent.on(d,"click",L.DomEvent.stopPropagation):(L.DomEvent.disableClickPropagation(d),L.DomEvent.on(d,"mousewheel",L.DomEvent.stopPropagation)),a(d,b),d}},c=function(a,b){return function(){for(var c=L.DomUtil.create("div",b),d=0;d
        '+a.labels[d]+"
        ";return L.Browser.touch?L.DomEvent.on(c,"click",L.DomEvent.stopPropagation):(L.DomEvent.disableClickPropagation(c),L.DomEvent.on(c,"mousewheel",L.DomEvent.stopPropagation)),c}};return{getOnAddArcGISLegend:b,getOnAddArrayLegend:c,updateArcGISLegend:a}}),angular.module("leaflet-directive").factory("leafletPathsHelpers",["$rootScope","$log","leafletHelpers",function(a,b,c){function d(a){return a.filter(function(a){return k(a)}).map(function(a){return new L.LatLng(a.lat,a.lng)})}function e(a){return new L.LatLng(a.lat,a.lng)}function f(a){return a.map(function(a){return d(a)})}function g(a,b){for(var c=["stroke","weight","color","opacity","fill","fillColor","fillOpacity","dashArray","lineCap","lineJoin","clickable","pointerEvents","className","smoothFactor","noClip"],d={},e=0;e
        ',controller:["$scope",function(b){this._leafletMap=a.defer(),this.getMap=function(){return this._leafletMap.promise},this.getLeafletScope=function(){return b}}],link:function(a,f,g,h){function i(){isNaN(g.width)?f.css("width",g.width):f.css("width",g.width+"px")}function j(){isNaN(g.height)?f.css("height",g.height):f.css("height",g.height+"px")}var k=d.isDefined,l=c.setDefaults(a.defaults,g.id),m=e.getAvailableMapEvents(),n=e.addEvents;a.mapId=g.id,b.setDirectiveControls({},g.id),k(g.width)&&(i(),a.$watch(function(){return f[0].getAttribute("width")},function(){i(),o.invalidateSize()})),k(g.height)&&(j(),a.$watch(function(){return f[0].getAttribute("height")},function(){j(),o.invalidateSize()}));var o=new L.Map(f[0],c.getMapCreationDefaults(g.id));if(h._leafletMap.resolve(o),k(g.center)||k(g.lfCenter)||o.setView([l.center.lat,l.center.lng],l.center.zoom),!k(g.tiles)&&!k(g.layers)){var p=L.tileLayer(l.tileLayer,l.tileLayerOptions);p.addTo(o),b.setTiles(p,g.id)}if(k(o.zoomControl)&&k(l.zoomControlPosition)&&o.zoomControl.setPosition(l.zoomControlPosition),k(o.zoomControl)&&l.zoomControl===!1&&o.zoomControl.removeFrom(o),k(o.zoomsliderControl)&&k(l.zoomsliderControl)&&l.zoomsliderControl===!1&&o.zoomsliderControl.removeFrom(o),!k(g.eventBroadcast)){var q="broadcast";n(o,m,"eventName",a,q)}o.whenReady(function(){b.setMap(o,g.id)}),a.$on("$destroy",function(){c.reset(),o.remove(),b.unresolveMap(g.id)}),a.$on("invalidateSize",function(){o.invalidateSize()})}}}]),angular.module("leaflet-directive").factory("leafletBoundsHelpers",["$log","leafletHelpers",function(a,b){function c(a){return angular.isDefined(a)&&angular.isDefined(a.southWest)&&angular.isDefined(a.northEast)&&angular.isNumber(a.southWest.lat)&&angular.isNumber(a.southWest.lng)&&angular.isNumber(a.northEast.lat)&&angular.isNumber(a.northEast.lng)}var d=b.isArray,e=b.isNumber,f=b.isFunction,g=b.isDefined;return{createLeafletBounds:function(a){return c(a)?L.latLngBounds([a.southWest.lat,a.southWest.lng],[a.northEast.lat,a.northEast.lng]):void 0},isValidBounds:c,createBoundsFromArray:function(b){return d(b)&&2===b.length&&d(b[0])&&d(b[1])&&2===b[0].length&&2===b[1].length&&e(b[0][0])&&e(b[0][1])&&e(b[1][0])&&e(b[1][1])?{northEast:{lat:b[0][0],lng:b[0][1]},southWest:{lat:b[1][0],lng:b[1][1]}}:void a.error("[AngularJS - Leaflet] The bounds array is not valid.")},createBoundsFromLeaflet:function(b){if(!(g(b)&&f(b.getNorthEast)&&f(b.getSouthWest)))return void a.error("[AngularJS - Leaflet] The leaflet bounds is not valid object.");var c=b.getNorthEast(),d=b.getSouthWest();return{northEast:{lat:c.lat,lng:c.lng},southWest:{lat:d.lat,lng:d.lng}}}}}]),angular.module("leaflet-directive").factory("leafletControlHelpers",["$rootScope","$log","leafletHelpers","leafletLayerHelpers","leafletMapDefaults",function(a,b,c,d,e){var f=c.isDefined,g=c.isObject,h=d.createLayer,i={},j=c.errorHeader+" [Controls] ",k=function(a,b,c){var d=e.getDefaults(c);if(!d.controls.layers.visible)return!1;var h=!1;return g(a)&&Object.keys(a).forEach(function(b){var c=a[b];f(c.layerOptions)&&c.layerOptions.showOnSelector===!1||(h=!0)}),g(b)&&Object.keys(b).forEach(function(a){var c=b[a];f(c.layerParams)&&c.layerParams.showOnSelector===!1||(h=!0)}),h},l=function(a){var b=e.getDefaults(a),c={collapsed:b.controls.layers.collapsed,position:b.controls.layers.position,autoZIndex:!1};angular.extend(c,b.controls.layers.options);var d;return d=b.controls.layers&&f(b.controls.layers.control)?b.controls.layers.control.apply(this,[[],[],c]):new L.control.layers([],[],c)},m={draw:{isPluginLoaded:function(){return angular.isDefined(L.Control.Draw)?!0:(b.error(j+" Draw plugin is not loaded."),!1)},checkValidParams:function(){return!0},createControl:function(a){return new L.Control.Draw(a)}},scale:{isPluginLoaded:function(){return!0},checkValidParams:function(){return!0},createControl:function(a){return new L.control.scale(a)}},fullscreen:{isPluginLoaded:function(){return angular.isDefined(L.Control.Fullscreen)?!0:(b.error(j+" Fullscreen plugin is not loaded."),!1)},checkValidParams:function(){return!0},createControl:function(a){return new L.Control.Fullscreen(a)}},search:{isPluginLoaded:function(){return angular.isDefined(L.Control.Search)?!0:(b.error(j+" Search plugin is not loaded."),!1)},checkValidParams:function(){return!0},createControl:function(a){return new L.Control.Search(a)}},custom:{},minimap:{isPluginLoaded:function(){return angular.isDefined(L.Control.MiniMap)?!0:(b.error(j+" Minimap plugin is not loaded."),!1)},checkValidParams:function(a){return f(a.layer)?!0:(b.warn(j+' minimap "layer" option should be defined.'),!1)},createControl:function(a){var c=h(a.layer);return f(c)?new L.Control.MiniMap(c,a):void b.warn(j+' minimap control "layer" could not be created.')}}};return{layersControlMustBeVisible:k,isValidControlType:function(a){return-1!==Object.keys(m).indexOf(a)},createControl:function(a,b){return m[a].checkValidParams(b)?m[a].createControl(b):void 0},updateLayersControl:function(a,b,c,d,e,g){var h,j=i[b],m=k(d,e,b);if(f(j)&&c){for(h in g.baselayers)j.removeLayer(g.baselayers[h]);for(h in g.overlays)j.removeLayer(g.overlays[h]);a.removeControl(j),delete i[b]}if(m){j=l(b),i[b]=j;for(h in d){var n=f(d[h].layerOptions)&&d[h].layerOptions.showOnSelector===!1;!n&&f(g.baselayers[h])&&j.addBaseLayer(g.baselayers[h],d[h].name)}for(h in e){var o=f(e[h].layerParams)&&e[h].layerParams.showOnSelector===!1;!o&&f(g.overlays[h])&&j.addOverlay(g.overlays[h],e[h].name)}a.addControl(j)}return m}}}]),angular.module("leaflet-directive").service("leafletData",["$log","$q","leafletHelpers",function(a,b,c){var d=c.getDefer,e=c.getUnresolvedDefer,f=c.setResolvedDefer,g={},h=this,i=function(a){return a.charAt(0).toUpperCase()+a.slice(1)},j=["map","tiles","layers","paths","markers","geoJSON","UTFGrid","decorations","directiveControls"];j.forEach(function(a){g[a]={}}),this.unresolveMap=function(a){var b=c.obtainEffectiveMapId(g.map,a);j.forEach(function(a){g[a][b]=void 0})},j.forEach(function(a){var b=i(a);h["set"+b]=function(b,c){var d=e(g[a],c);d.resolve(b),f(g[a],c)},h["get"+b]=function(b){var c=d(g[a],b);return c.promise}})}]),angular.module("leaflet-directive").service("leafletDirectiveControlsHelpers",["$log","leafletData","leafletHelpers",function(a,b,c){var d=c.isDefined,e=c.isString,f=c.isObject,g=c.errorHeader,h=g+"[leafletDirectiveControlsHelpers",i=function(c,g,i,j){var k=h+".extend] ",l={};if(!d(g))return void a.error(k+"thingToAddName cannot be undefined");if(e(g)&&d(i)&&d(j))l[g]={create:i,clean:j};else{if(!f(g)||d(i)||d(j))return void a.error(k+"incorrect arguments");l=g}b.getDirectiveControls().then(function(a){angular.extend(a,l),b.setDirectiveControls(a,c)})};return{extend:i}}]),angular.module("leaflet-directive").service("leafletGeoJsonHelpers",["leafletHelpers","leafletIterators",function(a,b){var c=a,d=b,e=function(a,b){return this.lat=a,this.lng=b,this},f=function(a){return Array.isArray(a)&&2===a.length?a[1]:c.isDefined(a.type)&&"Point"===a.type?+a.coordinates[1]:+a.lat},g=function(a){return Array.isArray(a)&&2===a.length?a[0]:c.isDefined(a.type)&&"Point"===a.type?+a.coordinates[0]:+a.lng},h=function(a){if(c.isUndefined(a))return!1;if(c.isArray(a)){if(2===a.length&&c.isNumber(a[0])&&c.isNumber(a[1]))return!0}else if(c.isDefined(a.type)&&"Point"===a.type&&c.isArray(a.coordinates)&&2===a.coordinates.length&&c.isNumber(a.coordinates[0])&&c.isNumber(a.coordinates[1]))return!0;var b=d.all(["lat","lng"],function(b){return c.isDefined(a[b])&&c.isNumber(a[b])});return b},i=function(a){if(a&&h(a)){var b=null;if(Array.isArray(a)&&2===a.length)b=new e(a[1],a[0]);else{if(!c.isDefined(a.type)||"Point"!==a.type)return a;b=new e(a.coordinates[1],a.coordinates[0])}return angular.extend(a,b)}};return{getLat:f,getLng:g,validateCoords:h,getCoords:i}}]),angular.module("leaflet-directive").service("leafletHelpers",["$q","$log",function(a,b){function c(a,c){var d,f;if(angular.isDefined(c))d=c;else if(0===Object.keys(a).length)d="main";else if(Object.keys(a).length>=1)for(f in a)a.hasOwnProperty(f)&&(d=f);else b.error(e+"- You have more than 1 map on the DOM, you must provide the map ID to the leafletData.getXXX call");return d}function d(b,d){var e,f=c(b,d);return angular.isDefined(b[f])&&b[f].resolvedDefer!==!0?e=b[f].defer:(e=a.defer(),b[f]={defer:e,resolvedDefer:!1}),e}var e="[AngularJS - Leaflet] ",f=angular.copy,g=f,h=function(a,b){var c;if(a&&angular.isObject(a))return null!==b&&angular.isString(b)?(c=a,b.split(".").forEach(function(a){c&&(c=c[a])}),c):b},i=function(a){return a.split(".").reduce(function(a,b){return a+'["'+b+'"]'})},j=function(a){return a.reduce(function(a,b){return a+"."+b})},k=function(a){return angular.isDefined(a)&&null!==a},l=function(a){return!k(a)},m=/([\:\-\_]+(.))/g,n=/^moz([A-Z])/,o=/^((?:x|data)[\:\-_])/i,p=function(a){return a.replace(m,function(a,b,c,d){return d?c.toUpperCase():c}).replace(n,"Moz$1")},q=function(a){return p(a.replace(o,""))};return{camelCase:p,directiveNormalize:q,copy:f,clone:g,errorHeader:e,getObjectValue:h,getObjectArrayPath:i,getObjectDotPath:j,defaultTo:function(a,b){return k(a)?a:b},isTruthy:function(a){return"true"===a||a===!0},isEmpty:function(a){return 0===Object.keys(a).length},isUndefinedOrEmpty:function(a){return angular.isUndefined(a)||null===a||0===Object.keys(a).length},isDefined:k,isUndefined:l,isNumber:angular.isNumber,isString:angular.isString,isArray:angular.isArray,isObject:angular.isObject,isFunction:angular.isFunction,equals:angular.equals,isValidCenter:function(a){return angular.isDefined(a)&&angular.isNumber(a.lat)&&angular.isNumber(a.lng)&&angular.isNumber(a.zoom)},isValidPoint:function(a){return angular.isDefined(a)?angular.isArray(a)?2===a.length&&angular.isNumber(a[0])&&angular.isNumber(a[1]):angular.isNumber(a.lat)&&angular.isNumber(a.lng):!1},isSameCenterOnMap:function(a,b){var c=b.getCenter(),d=b.getZoom();return a.lat&&a.lng&&c.lat.toFixed(4)===a.lat.toFixed(4)&&c.lng.toFixed(4)===a.lng.toFixed(4)&&d===a.zoom?!0:!1},safeApply:function(a,b){var c=a.$root.$$phase;"$apply"===c||"$digest"===c?a.$eval(b):a.$evalAsync(b)},obtainEffectiveMapId:c,getDefer:function(a,b){var e,f=c(a,b);return e=angular.isDefined(a[f])&&a[f].resolvedDefer!==!1?a[f].defer:d(a,b)},getUnresolvedDefer:d,setResolvedDefer:function(a,b){var d=c(a,b);a[d].resolvedDefer=!0},rangeIsSupported:function(){var a=document.createElement("input");return a.setAttribute("type","range"),"range"===a.type},FullScreenControlPlugin:{isLoaded:function(){return angular.isDefined(L.Control.Fullscreen)}},MiniMapControlPlugin:{isLoaded:function(){return angular.isDefined(L.Control.MiniMap)}},AwesomeMarkersPlugin:{isLoaded:function(){return angular.isDefined(L.AwesomeMarkers)&&angular.isDefined(L.AwesomeMarkers.Icon)},is:function(a){return this.isLoaded()?a instanceof L.AwesomeMarkers.Icon:!1},equal:function(a,b){return this.isLoaded()&&this.is(a)?angular.equals(a,b):!1}},VectorMarkersPlugin:{isLoaded:function(){return angular.isDefined(L.VectorMarkers)&&angular.isDefined(L.VectorMarkers.Icon)},is:function(a){return this.isLoaded()?a instanceof L.VectorMarkers.Icon:!1},equal:function(a,b){return this.isLoaded()&&this.is(a)?angular.equals(a,b):!1}},DomMarkersPlugin:{isLoaded:function(){return angular.isDefined(L.DomMarkers)&&angular.isDefined(L.DomMarkers.Icon)?!0:!1},is:function(a){return this.isLoaded()?a instanceof L.DomMarkers.Icon:!1},equal:function(a,b){return this.isLoaded()&&this.is(a)?angular.equals(a,b):!1}},PolylineDecoratorPlugin:{isLoaded:function(){return angular.isDefined(L.PolylineDecorator)?!0:!1},is:function(a){return this.isLoaded()?a instanceof L.PolylineDecorator:!1},equal:function(a,b){return this.isLoaded()&&this.is(a)?angular.equals(a,b):!1}},MakiMarkersPlugin:{isLoaded:function(){return angular.isDefined(L.MakiMarkers)&&angular.isDefined(L.MakiMarkers.Icon)?!0:!1},is:function(a){return this.isLoaded()?a instanceof L.MakiMarkers.Icon:!1},equal:function(a,b){return this.isLoaded()&&this.is(a)?angular.equals(a,b):!1}},ExtraMarkersPlugin:{isLoaded:function(){return angular.isDefined(L.ExtraMarkers)&&angular.isDefined(L.ExtraMarkers.Icon)?!0:!1},is:function(a){return this.isLoaded()?a instanceof L.ExtraMarkers.Icon:!1},equal:function(a,b){return this.isLoaded()&&this.is(a)?angular.equals(a,b):!1}},LabelPlugin:{isLoaded:function(){return angular.isDefined(L.Label)},is:function(a){return this.isLoaded()?a instanceof L.MarkerClusterGroup:!1}},MarkerClusterPlugin:{isLoaded:function(){return angular.isDefined(L.MarkerClusterGroup)},is:function(a){return this.isLoaded()?a instanceof L.MarkerClusterGroup:!1}},GoogleLayerPlugin:{isLoaded:function(){return angular.isDefined(L.Google)},is:function(a){return this.isLoaded()?a instanceof L.Google:!1}},LeafletProviderPlugin:{isLoaded:function(){return angular.isDefined(L.TileLayer.Provider)},is:function(a){return this.isLoaded()?a instanceof L.TileLayer.Provider:!1}},ChinaLayerPlugin:{isLoaded:function(){return angular.isDefined(L.tileLayer.chinaProvider)}},HeatLayerPlugin:{isLoaded:function(){return angular.isDefined(L.heatLayer)}},WebGLHeatMapLayerPlugin:{isLoaded:function(){return angular.isDefined(L.TileLayer.WebGLHeatMap)}},BingLayerPlugin:{isLoaded:function(){return angular.isDefined(L.BingLayer)},is:function(a){return this.isLoaded()?a instanceof L.BingLayer:!1}},WFSLayerPlugin:{isLoaded:function(){return void 0!==L.GeoJSON.WFS},is:function(a){return this.isLoaded()?a instanceof L.GeoJSON.WFS:!1}},AGSBaseLayerPlugin:{isLoaded:function(){return void 0!==L.esri&&void 0!==L.esri.basemapLayer},is:function(a){return this.isLoaded()?a instanceof L.esri.basemapLayer:!1}},AGSLayerPlugin:{isLoaded:function(){return void 0!==lvector&&void 0!==lvector.AGS},is:function(a){return this.isLoaded()?a instanceof lvector.AGS:!1}},AGSFeatureLayerPlugin:{isLoaded:function(){return void 0!==L.esri&&void 0!==L.esri.featureLayer},is:function(a){return this.isLoaded()?a instanceof L.esri.featureLayer:!1}},AGSTiledMapLayerPlugin:{isLoaded:function(){return void 0!==L.esri&&void 0!==L.esri.tiledMapLayer},is:function(a){return this.isLoaded()?a instanceof L.esri.tiledMapLayer:!1}},AGSDynamicMapLayerPlugin:{isLoaded:function(){return void 0!==L.esri&&void 0!==L.esri.dynamicMapLayer},is:function(a){return this.isLoaded()?a instanceof L.esri.dynamicMapLayer:!1}},AGSImageMapLayerPlugin:{isLoaded:function(){return void 0!==L.esri&&void 0!==L.esri.imageMapLayer},is:function(a){return this.isLoaded()?a instanceof L.esri.imageMapLayer:!1}},AGSClusteredLayerPlugin:{isLoaded:function(){return void 0!==L.esri&&void 0!==L.esri.clusteredFeatureLayer},is:function(a){return this.isLoaded()?a instanceof L.esri.clusteredFeatureLayer:!1}},AGSHeatmapLayerPlugin:{isLoaded:function(){return void 0!==L.esri&&void 0!==L.esri.heatmapFeatureLayer},is:function(a){return this.isLoaded()?a instanceof L.esri.heatmapFeatureLayer:!1}},YandexLayerPlugin:{isLoaded:function(){return angular.isDefined(L.Yandex)},is:function(a){return this.isLoaded()?a instanceof L.Yandex:!1}},GeoJSONPlugin:{isLoaded:function(){return angular.isDefined(L.TileLayer.GeoJSON)},is:function(a){return this.isLoaded()?a instanceof L.TileLayer.GeoJSON:!1}},UTFGridPlugin:{isLoaded:function(){return angular.isDefined(L.UtfGrid)},is:function(a){return this.isLoaded()?a instanceof L.UtfGrid:(b.error("[AngularJS - Leaflet] No UtfGrid plugin found."),!1)}},CartoDB:{isLoaded:function(){return cartodb},is:function(){return!0}},Leaflet:{DivIcon:{is:function(a){return a instanceof L.DivIcon},equal:function(a,b){return this.is(a)?angular.equals(a,b):!1}},Icon:{is:function(a){return a instanceof L.Icon},equal:function(a,b){return this.is(a)?angular.equals(a,b):!1}}},watchOptions:{doWatch:!0,isDeep:!0,individual:{doWatch:!0,isDeep:!0}}}}]),angular.module("leaflet-directive").service("leafletIterators",["$log","leafletHelpers",function(a,b){var c,d=b,e=b.errorHeader+"leafletIterators: ",f=Object.keys,g=d.isFunction,h=d.isObject,i=Math.pow(2,53)-1,j=function(a){var b=null!==a&&a.length;return d.isNumber(b)&&b>=0&&i>=b},k=function(a){return a},l=function(a){return function(b){return null===b?void 0:b[a]}},m=function(a,b,c){if(void 0===b)return a;switch(null===c?3:c){case 1:return function(c){return a.call(b,c)};case 2:return function(c,d){return a.call(b,c,d)};case 3:return function(c,d,e){return a.call(b,c,d,e)};case 4:return function(c,d,e,f){return a.call(b,c,d,e,f)}}return function(){return a.apply(b,arguments)}},n=function(a,b){return function(c){var d=arguments.length;if(2>d||null===c)return c;for(var e=1;d>e;e++)for(var f=arguments[e],g=a(f),h=g.length,i=0;h>i;i++){var j=g[i];b&&void 0!==c[j]||(c[j]=f[j])}return c}},o=null;c=o=n(f);var p,q=function(a,b){var c=f(b),d=c.length;if(null===a)return!d;for(var e=Object(a),g=0;d>g;g++){var h=c[g];if(b[h]!==e[h]||!(h in e))return!1}return!0},r=null;p=r=function(a){return a=c({},a),function(b){return q(b,a)}};var s,t=function(a,b,c){return null===a?k:g(a)?m(a,b,c):h(a)?p(a):l(a)},u=null;s=u=function(a,b,c){b=t(b,c);for(var d=!j(a)&&f(a),e=(d||a).length,g=0;e>g;g++){var h=d?d[g]:g;if(!b(a[h],h,a))return!1}return!0};var v=function(b,c,f,g){return f||d.isDefined(b)&&d.isDefined(c)?d.isFunction(c)?!1:(g=d.defaultTo(c,"cb"),a.error(e+g+" is not a function"),!0):!0},w=function(a,b,c){if(!v(void 0,c,!0,"internalCb")&&!v(a,b))for(var d in a)a.hasOwnProperty(d)&&c(a[d],d)},x=function(a,b){w(a,b,function(a,c){b(a,c)})};return{each:x,forEach:x,every:s,all:u}}]),angular.module("leaflet-directive").factory("leafletLayerHelpers",["$rootScope","$log","$q","leafletHelpers","leafletIterators",function($rootScope,$log,$q,leafletHelpers,leafletIterators){function isValidLayerType(a){return isString(a.type)?-1===Object.keys(layerTypes).indexOf(a.type)?($log.error("[AngularJS - Leaflet] A layer must have a valid type: "+Object.keys(layerTypes)),!1):layerTypes[a.type].mustHaveUrl&&!isString(a.url)?($log.error("[AngularJS - Leaflet] A base layer must have an url"),!1):layerTypes[a.type].mustHaveData&&!isDefined(a.data)?($log.error('[AngularJS - Leaflet] The base layer must have a "data" array attribute'),!1):layerTypes[a.type].mustHaveLayer&&!isDefined(a.layer)?($log.error("[AngularJS - Leaflet] The type of layer "+a.type+" must have an layer defined"),!1):layerTypes[a.type].mustHaveBounds&&!isDefined(a.bounds)?($log.error("[AngularJS - Leaflet] The type of layer "+a.type+" must have bounds defined"),!1):layerTypes[a.type].mustHaveKey&&!isDefined(a.key)?($log.error("[AngularJS - Leaflet] The type of layer "+a.type+" must have key defined"),!1):!0:($log.error("[AngularJS - Leaflet] A layer must have a valid type defined."),!1)}function createLayer(a){if(isValidLayerType(a)){if(!isString(a.name))return void $log.error("[AngularJS - Leaflet] A base layer must have a name");isObject(a.layerParams)||(a.layerParams={}),isObject(a.layerOptions)||(a.layerOptions={});for(var b in a.layerParams)a.layerOptions[b]=a.layerParams[b];var c={url:a.url,data:a.data,options:a.layerOptions,layer:a.layer,icon:a.icon,type:a.layerType,bounds:a.bounds,key:a.key,apiKey:a.apiKey,pluginOptions:a.pluginOptions,user:a.user};return layerTypes[a.type].createLayer(c)}}function safeAddLayer(a,b){b&&"function"==typeof b.addTo?b.addTo(a):a.addLayer(b)}function safeRemoveLayer(a,b,c){if(isDefined(c)&&isDefined(c.loadedDefer))if(angular.isFunction(c.loadedDefer)){var d=c.loadedDefer();$log.debug("Loaded Deferred",d);var e=d.length;if(e>0)for(var f=function(){e--,0===e&&a.removeLayer(b)},g=0;g'+b.error.message+"";else if("arcgis"===c)for(var e=0;e'+f.layerName+"";for(var g=0;g
        '+h.label+"
        "}}else"image"===c&&(a.innerHTML='')},b=function(b,c,d,e){return function(){var f=L.DomUtil.create("div",c);return L.Browser.touch?L.DomEvent.on(f,"click",L.DomEvent.stopPropagation):(L.DomEvent.disableClickPropagation(f),L.DomEvent.on(f,"mousewheel",L.DomEvent.stopPropagation)),a(f,b,d,e),f}},c=function(a,b){return function(){for(var c=L.DomUtil.create("div",b),d=0;d
        '+a.labels[d]+"
        ";return L.Browser.touch?L.DomEvent.on(c,"click",L.DomEvent.stopPropagation):(L.DomEvent.disableClickPropagation(c),L.DomEvent.on(c,"mousewheel",L.DomEvent.stopPropagation)),c}};return{getOnAddLegend:b,getOnAddArrayLegend:c,updateLegend:a}}),angular.module("leaflet-directive").factory("leafletMapDefaults",["$q","leafletHelpers",function(a,b){function c(){return{keyboard:!0,dragging:!0,worldCopyJump:!1,doubleClickZoom:!0,scrollWheelZoom:!0,tap:!0,touchZoom:!0,zoomControl:!0,zoomsliderControl:!1,zoomControlPosition:"topleft",attributionControl:!0,controls:{layers:{visible:!0,position:"topright",collapsed:!0}},nominatim:{server:" http://nominatim.openstreetmap.org/search"},crs:L.CRS.EPSG3857,tileLayer:"//{s}.tile.openstreetmap.org/{z}/{x}/{y}.png",tileLayerOptions:{attribution:'© OpenStreetMap contributors'},path:{weight:10,opacity:1,color:"#0000ff"},center:{lat:0,lng:0,zoom:1}}}var d=b.isDefined,e=b.isObject,f=b.obtainEffectiveMapId,g={};return{reset:function(){g={}},getDefaults:function(a){var b=f(g,a);return g[b]},getMapCreationDefaults:function(a){var b=f(g,a),c=g[b],e={maxZoom:c.maxZoom,keyboard:c.keyboard,dragging:c.dragging,zoomControl:c.zoomControl,doubleClickZoom:c.doubleClickZoom,scrollWheelZoom:c.scrollWheelZoom,tap:c.tap,touchZoom:c.touchZoom,attributionControl:c.attributionControl,worldCopyJump:c.worldCopyJump,crs:c.crs};if(d(c.minZoom)&&(e.minZoom=c.minZoom),d(c.zoomAnimation)&&(e.zoomAnimation=c.zoomAnimation),d(c.fadeAnimation)&&(e.fadeAnimation=c.fadeAnimation),d(c.markerZoomAnimation)&&(e.markerZoomAnimation=c.markerZoomAnimation),c.map)for(var h in c.map)e[h]=c.map[h];return e},setDefaults:function(a,b){var h=c();d(a)&&(h.doubleClickZoom=d(a.doubleClickZoom)?a.doubleClickZoom:h.doubleClickZoom,h.scrollWheelZoom=d(a.scrollWheelZoom)?a.scrollWheelZoom:h.doubleClickZoom,h.tap=d(a.tap)?a.tap:h.tap,h.touchZoom=d(a.touchZoom)?a.touchZoom:h.doubleClickZoom,h.zoomControl=d(a.zoomControl)?a.zoomControl:h.zoomControl,h.zoomsliderControl=d(a.zoomsliderControl)?a.zoomsliderControl:h.zoomsliderControl,h.attributionControl=d(a.attributionControl)?a.attributionControl:h.attributionControl,h.tileLayer=d(a.tileLayer)?a.tileLayer:h.tileLayer,h.zoomControlPosition=d(a.zoomControlPosition)?a.zoomControlPosition:h.zoomControlPosition,h.keyboard=d(a.keyboard)?a.keyboard:h.keyboard,h.dragging=d(a.dragging)?a.dragging:h.dragging,d(a.controls)&&angular.extend(h.controls,a.controls),e(a.crs)?h.crs=a.crs:d(L.CRS[a.crs])&&(h.crs=L.CRS[a.crs]),d(a.center)&&angular.copy(a.center,h.center),d(a.tileLayerOptions)&&angular.copy(a.tileLayerOptions,h.tileLayerOptions),d(a.maxZoom)&&(h.maxZoom=a.maxZoom),d(a.minZoom)&&(h.minZoom=a.minZoom),d(a.zoomAnimation)&&(h.zoomAnimation=a.zoomAnimation),d(a.fadeAnimation)&&(h.fadeAnimation=a.fadeAnimation),d(a.markerZoomAnimation)&&(h.markerZoomAnimation=a.markerZoomAnimation),d(a.worldCopyJump)&&(h.worldCopyJump=a.worldCopyJump),d(a.map)&&(h.map=a.map),d(a.path)&&(h.path=a.path));var i=f(g,b);return g[i]=h,h}}}]),angular.module("leaflet-directive").service("leafletMarkersHelpers",["$rootScope","$timeout","leafletHelpers","$log","$compile","leafletGeoJsonHelpers",function(a,b,c,d,e,f){var g=c.isDefined,h=c.defaultTo,i=c.MarkerClusterPlugin,j=c.AwesomeMarkersPlugin,k=c.VectorMarkersPlugin,l=c.MakiMarkersPlugin,m=c.ExtraMarkersPlugin,n=c.DomMarkersPlugin,o=c.safeApply,p=c,q=c.isString,r=c.isNumber,s=c.isObject,t={},u=f,v=c.errorHeader,w=function(a){ +var b="";return["_icon","_latlng","_leaflet_id","_map","_shadow"].forEach(function(c){b+=c+": "+h(a[c],"undefined")+" \n"}),"[leafletMarker] : \n"+b},x=function(a,b){var c=b?console:d;c.debug(w(a))},y=function(b){if(g(b)&&g(b.type)&&"awesomeMarker"===b.type)return j.isLoaded()||d.error(v+" The AwesomeMarkers Plugin is not loaded."),new L.AwesomeMarkers.icon(b);if(g(b)&&g(b.type)&&"vectorMarker"===b.type)return k.isLoaded()||d.error(v+" The VectorMarkers Plugin is not loaded."),new L.VectorMarkers.icon(b);if(g(b)&&g(b.type)&&"makiMarker"===b.type)return l.isLoaded()||d.error(v+"The MakiMarkers Plugin is not loaded."),new L.MakiMarkers.icon(b);if(g(b)&&g(b.type)&&"extraMarker"===b.type)return m.isLoaded()||d.error(v+"The ExtraMarkers Plugin is not loaded."),new L.ExtraMarkers.icon(b);if(g(b)&&g(b.type)&&"div"===b.type)return new L.divIcon(b);if(g(b)&&g(b.type)&&"dom"===b.type){n.isLoaded()||d.error(v+"The DomMarkers Plugin is not loaded.");var c=angular.isFunction(b.getMarkerScope)?b.getMarkerScope():a,f=e(b.template)(c),h=angular.copy(b);return h.element=f[0],new L.DomMarkers.icon(h)}if(g(b)&&g(b.type)&&"icon"===b.type)return b.icon;var i="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABkAAAApCAYAAADAk4LOAAAGmklEQVRYw7VXeUyTZxjvNnfELFuyIzOabermMZEeQC/OclkO49CpOHXOLJl/CAURuYbQi3KLgEhbrhZ1aDwmaoGqKII6odATmH/scDFbdC7LvFqOCc+e95s2VG50X/LLm/f4/Z7neY/ne18aANCmAr5E/xZf1uDOkTcGcWR6hl9247tT5U7Y6SNvWsKT63P58qbfeLJG8M5qcgTknrvvrdDbsT7Ml+tv82X6vVxJE33aRmgSyYtcWVMqX97Yv2JvW39UhRE2HuyBL+t+gK1116ly06EeWFNlAmHxlQE0OMiV6mQCScusKRlhS3QLeVJdl1+23h5dY4FNB3thrbYboqptEFlphTC1hSpJnbRvxP4NWgsE5Jyz86QNNi/5qSUTGuFk1gu54tN9wuK2wc3o+Wc13RCmsoBwEqzGcZsxsvCSy/9wJKf7UWf1mEY8JWfewc67UUoDbDjQC+FqK4QqLVMGGR9d2wurKzqBk3nqIT/9zLxRRjgZ9bqQgub+DdoeCC03Q8j+0QhFhBHR/eP3U/zCln7Uu+hihJ1+bBNffLIvmkyP0gpBZWYXhKussK6mBz5HT6M1Nqpcp+mBCPXosYQfrekGvrjewd59/GvKCE7TbK/04/ZV5QZYVWmDwH1mF3xa2Q3ra3DBC5vBT1oP7PTj4C0+CcL8c7C2CtejqhuCnuIQHaKHzvcRfZpnylFfXsYJx3pNLwhKzRAwAhEqG0SpusBHfAKkxw3w4627MPhoCH798z7s0ZnBJ/MEJbZSbXPhER2ih7p2ok/zSj2cEJDd4CAe+5WYnBCgR2uruyEw6zRoW6/DWJ/OeAP8pd/BGtzOZKpG8oke0SX6GMmRk6GFlyAc59K32OTEinILRJRchah8HQwND8N435Z9Z0FY1EqtxUg+0SO6RJ/mmXz4VuS+DpxXC3gXmZwIL7dBSH4zKE50wESf8qwVgrP1EIlTO5JP9Igu0aexdh28F1lmAEGJGfh7jE6ElyM5Rw/FDcYJjWhbeiBYoYNIpc2FT/SILivp0F1ipDWk4BIEo2VuodEJUifhbiltnNBIXPUFCMpthtAyqws/BPlEF/VbaIxErdxPphsU7rcCp8DohC+GvBIPJS/tW2jtvTmmAeuNO8BNOYQeG8G/2OzCJ3q+soYB5i6NhMaKr17FSal7GIHheuV3uSCY8qYVuEm1cOzqdWr7ku/R0BDoTT+DT+ohCM6/CCvKLKO4RI+dXPeAuaMqksaKrZ7L3FE5FIFbkIceeOZ2OcHO6wIhTkNo0ffgjRGxEqogXHYUPHfWAC/lADpwGcLRY3aeK4/oRGCKYcZXPVoeX/kelVYY8dUGf8V5EBRbgJXT5QIPhP9ePJi428JKOiEYhYXFBqou2Guh+p/mEB1/RfMw6rY7cxcjTrneI1FrDyuzUSRm9miwEJx8E/gUmqlyvHGkneiwErR21F3tNOK5Tf0yXaT+O7DgCvALTUBXdM4YhC/IawPU+2PduqMvuaR6eoxSwUk75ggqsYJ7VicsnwGIkZBSXKOUww73WGXyqP+J2/b9c+gi1YAg/xpwck3gJuucNrh5JvDPvQr0WFXf0piyt8f8/WI0hV4pRxxkQZdJDfDJNOAmM0Ag8jyT6hz0WGXWuP94Yh2jcfjmXAGvHCMslRimDHYuHuDsy2QtHuIavznhbYURq5R57KpzBBRZKPJi8eQg48h4j8SDdowifdIrEVdU+gbO6QNvRRt4ZBthUaZhUnjlYObNagV3keoeru3rU7rcuceqU1mJBxy+BWZYlNEBH+0eH4vRiB+OYybU2hnblYlTvkHinM4m54YnxSyaZYSF6R3jwgP7udKLGIX6r/lbNa9N6y5MFynjWDtrHd75ZvTYAPO/6RgF0k76mQla3FGq7dO+cH8sKn0Vo7nDllwAhqwLPkxrHwWmHJOo+AKJ4rab5OgrM7rVu8eWb2Pu0Dh4eDgXoOfvp7Y7QeqknRmvcTBEyq9m/HQQSCSz6LHq3z0yzsNySRfMS253wl2KyRDbcZPcfJKjZmSEOjcxyi+Y8dUOtsIEH6R2wNykdqrkYJ0RV92H0W58pkfQk7cKevsLK10Py8SdMGfXNXATY+pPbyJR/ET6n9nIfztNtZYRV9XniQu9IA2vOVgy4ir7GCLVmmd+zjkH0eAF9Po6K61pmCXHxU5rHMYd1ftc3owjwRSVRzLjKvqZEty6cRUD7jGqiOdu5HG6MdHjNcNYGqfDm5YRzLBBCCDl/2bk8a8gdbqcfwECu62Fg/HrggAAAABJRU5ErkJggg==",o="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACkAAAApCAYAAACoYAD2AAAC5ElEQVRYw+2YW4/TMBCF45S0S1luXZCABy5CgLQgwf//S4BYBLTdJLax0fFqmB07nnQfEGqkIydpVH85M+NLjPe++dcPc4Q8Qh4hj5D/AaQJx6H/4TMwB0PeBNwU7EGQAmAtsNfAzoZkgIa0ZgLMa4Aj6CxIAsjhjOCoL5z7Glg1JAOkaicgvQBXuncwJAWjksLtBTWZe04CnYRktUGdilALppZBOgHGZcBzL6OClABvMSVIzyBjazOgrvACf1ydC5mguqAVg6RhdkSWQFj2uxfaq/BrIZOLEWgZdALIDvcMcZLD8ZbLC9de4yR1sYMi4G20S4Q/PWeJYxTOZn5zJXANZHIxAd4JWhPIloTJZhzMQduM89WQ3MUVAE/RnhAXpTycqys3NZALOBbB7kFrgLesQl2h45Fcj8L1tTSohUwuxhy8H/Qg6K7gIs+3kkaigQCOcyEXCHN07wyQazhrmIulvKMQAwMcmLNqyCVyMAI+BuxSMeTk3OPikLY2J1uE+VHQk6ANrhds+tNARqBeaGc72cK550FP4WhXmFmcMGhTwAR1ifOe3EvPqIegFmF+C8gVy0OfAaWQPMR7gF1OQKqGoBjq90HPMP01BUjPOqGFksC4emE48tWQAH0YmvOgF3DST6xieJgHAWxPAHMuNhrImIdvoNOKNWIOcE+UXE0pYAnkX6uhWsgVXDxHdTfCmrEEmMB2zMFimLVOtiiajxiGWrbU52EeCdyOwPEQD8LqyPH9Ti2kgYMf4OhSKB7qYILbBv3CuVTJ11Y80oaseiMWOONc/Y7kJYe0xL2f0BaiFTxknHO5HaMGMublKwxFGzYdWsBF174H/QDknhTHmHHN39iWFnkZx8lPyM8WHfYELmlLKtgWNmFNzQcC1b47gJ4hL19i7o65dhH0Negbca8vONZoP7doIeOC9zXm8RjuL0Gf4d4OYaU5ljo3GYiqzrWQHfJxA6ALhDpVKv9qYeZA8eM3EhfPSCmpuD0AAAAASUVORK5CYII=";return g(b)&&g(b.iconUrl)?new L.Icon(b):new L.Icon.Default({iconUrl:i,shadowUrl:o,iconSize:[25,41],iconAnchor:[12,41],popupAnchor:[1,-34],shadowSize:[41,41]})},z=function(a){g(t[a])&&t.splice(a,1)},A=function(){t={}},B=function(a,b,c){if(a.closePopup(),g(c)&&g(c.overlays))for(var d in c.overlays)if((c.overlays[d]instanceof L.LayerGroup||c.overlays[d]instanceof L.FeatureGroup)&&c.overlays[d].hasLayer(a))return void c.overlays[d].removeLayer(a);if(g(t))for(var e in t)t[e].hasLayer(a)&&t[e].removeLayer(a);b.hasLayer(a)&&b.removeLayer(a)},C=function(a,b){var c=a._popup._container.offsetHeight,d=new L.Point(a._popup._containerLeft,-c-a._popup._containerBottom),e=b.layerPointToContainerPoint(d);null!==e&&a._popup._adjustPan()},D=function(a,b){e(a._popup._contentNode)(b)},E=function(a,c,d){var e=a._popup._contentNode.innerText||a._popup._contentNode.textContent;e.length<1&&b(function(){E(a,c,d)});var f=a._popup._contentNode.offsetWidth;return a._popup._updateLayout(),a._popup._updatePosition(),a._popup.options.autoPan&&C(a,d),f},F=function(b,c,e){var f=angular.isFunction(c.getMessageScope)?c.getMessageScope():a,h=g(c.compileMessage)?c.compileMessage:!0;if(h){if(!g(b._popup)||!g(b._popup._contentNode))return d.error(v+"Popup is invalid or does not have any content."),!1;D(b,f),E(b,c,e)}},G=function(b,c){var d=angular.isFunction(c.getMessageScope)?c.getMessageScope():a,f=angular.isFunction(c.getLabelScope)?c.getLabelScope():d,h=g(c.compileMessage)?c.compileMessage:!0;p.LabelPlugin.isLoaded()&&g(c.label)&&(g(c.label.options)&&c.label.options.noHide===!0&&b.showLabel(),h&&g(b.label)&&e(b.label._container)(f))},H=function(a,b,c,e,f,h,i){if(g(b)){if(!u.validateCoords(a))return d.warn("There are problems with lat-lng data, please verify your marker model"),void B(c,i,h);var j=a===b;if(g(a.iconAngle)&&b.iconAngle!==a.iconAngle&&c.setIconAngle(a.iconAngle),q(a.layer)||q(b.layer)&&(g(h.overlays[b.layer])&&h.overlays[b.layer].hasLayer(c)&&(h.overlays[b.layer].removeLayer(c),c.closePopup()),i.hasLayer(c)||i.addLayer(c)),(r(a.opacity)||r(parseFloat(a.opacity)))&&a.opacity!==b.opacity&&c.setOpacity(a.opacity),q(a.layer)&&b.layer!==a.layer){if(q(b.layer)&&g(h.overlays[b.layer])&&h.overlays[b.layer].hasLayer(c)&&h.overlays[b.layer].removeLayer(c),c.closePopup(),i.hasLayer(c)&&i.removeLayer(c),!g(h.overlays[a.layer]))return void d.error(v+"You must use a name of an existing layer");var k=h.overlays[a.layer];if(!(k instanceof L.LayerGroup||k instanceof L.FeatureGroup))return void d.error(v+'A marker can only be added to a layer of type "group" or "featureGroup"');k.addLayer(c),i.hasLayer(c)&&a.focus===!0&&c.openPopup()}if(a.draggable!==!0&&b.draggable===!0&&g(c.dragging)&&c.dragging.disable(),a.draggable===!0&&b.draggable!==!0&&(c.dragging?c.dragging.enable():L.Handler.MarkerDrag&&(c.dragging=new L.Handler.MarkerDrag(c),c.options.draggable=!0,c.dragging.enable())),s(a.icon)||s(b.icon)&&(c.setIcon(y()),c.closePopup(),c.unbindPopup(),q(a.message)&&c.bindPopup(a.message,a.popupOptions)),s(a.icon)&&s(b.icon)&&!angular.equals(a.icon,b.icon)){var l=!1;c.dragging&&(l=c.dragging.enabled()),c.setIcon(y(a.icon)),l&&c.dragging.enable(),c.closePopup(),c.unbindPopup(),q(a.message)&&(c.bindPopup(a.message,a.popupOptions),i.hasLayer(c)&&a.focus===!0&&c.openPopup())}!q(a.message)&&q(b.message)&&(c.closePopup(),c.unbindPopup()),p.LabelPlugin.isLoaded()&&(g(a.label)&&g(a.label.message)?"label"in b&&"message"in b.label&&!angular.equals(a.label.message,b.label.message)?c.updateLabelContent(a.label.message):!angular.isFunction(c.getLabel)||angular.isFunction(c.getLabel)&&!g(c.getLabel())?(c.bindLabel(a.label.message,a.label.options),G(c,a)):G(c,a):(!("label"in a)||"message"in a.label)&&angular.isFunction(c.unbindLabel)&&c.unbindLabel()),q(a.message)&&!q(b.message)&&c.bindPopup(a.message,a.popupOptions),q(a.message)&&q(b.message)&&a.message!==b.message&&c.setPopupContent(a.message);var m=!1;a.focus!==!0&&b.focus===!0&&(c.closePopup(),m=!0),(a.focus===!0&&(!g(b.focus)||b.focus===!1)||j&&a.focus===!0)&&(c.openPopup(),m=!0),b.zIndexOffset!==a.zIndexOffset&&c.setZIndexOffset(a.zIndexOffset);var n=c.getLatLng(),o=q(a.layer)&&p.MarkerClusterPlugin.is(h.overlays[a.layer]);o?m?(a.lat!==b.lat||a.lng!==b.lng)&&(h.overlays[a.layer].removeLayer(c),c.setLatLng([a.lat,a.lng]),h.overlays[a.layer].addLayer(c)):n.lat!==a.lat||n.lng!==a.lng?(h.overlays[a.layer].removeLayer(c),c.setLatLng([a.lat,a.lng]),h.overlays[a.layer].addLayer(c)):a.lat!==b.lat||a.lng!==b.lng?(h.overlays[a.layer].removeLayer(c),c.setLatLng([a.lat,a.lng]),h.overlays[a.layer].addLayer(c)):s(a.icon)&&s(b.icon)&&!angular.equals(a.icon,b.icon)&&(h.overlays[a.layer].removeLayer(c),h.overlays[a.layer].addLayer(c)):(n.lat!==a.lat||n.lng!==a.lng)&&c.setLatLng([a.lat,a.lng])}};return{resetMarkerGroup:z,resetMarkerGroups:A,deleteMarker:B,manageOpenPopup:F,manageOpenLabel:G,createMarker:function(a){if(!g(a)||!u.validateCoords(a))return void d.error(v+"The marker definition is not valid.");var b=u.getCoords(a);if(!g(b))return void d.error(v+"Unable to get coordinates from markerData.");var c={icon:y(a.icon),title:g(a.title)?a.title:"",draggable:g(a.draggable)?a.draggable:!1,clickable:g(a.clickable)?a.clickable:!0,riseOnHover:g(a.riseOnHover)?a.riseOnHover:!1,zIndexOffset:g(a.zIndexOffset)?a.zIndexOffset:0,iconAngle:g(a.iconAngle)?a.iconAngle:0};for(var e in a)a.hasOwnProperty(e)&&!c.hasOwnProperty(e)&&(c[e]=a[e]);var f=new L.marker(b,c);return q(a.message)||f.unbindPopup(),f},addMarkerToGroup:function(a,b,c,e){return q(b)?i.isLoaded()?(g(t[b])||(t[b]=new L.MarkerClusterGroup(c),e.addLayer(t[b])),void t[b].addLayer(a)):void d.error(v+"The MarkerCluster plugin is not loaded."):void d.error(v+"The marker group you have specified is invalid.")},listenMarkerEvents:function(a,b,c,d,e){a.on("popupopen",function(){o(c,function(){(g(a._popup)||g(a._popup._contentNode))&&(b.focus=!0,F(a,b,e))})}),a.on("popupclose",function(){o(c,function(){b.focus=!1})}),a.on("add",function(){o(c,function(){"label"in b&&G(a,b)})})},updateMarker:H,addMarkerWatcher:function(a,b,c,d,e,f){var i=p.getObjectArrayPath("markers."+b);f=h(f,!0);var j=c.$watch(i,function(f,h){return g(f)?void H(f,h,a,b,c,d,e):(B(a,e,d),void j())},f)},string:w,log:x}}]),angular.module("leaflet-directive").factory("leafletPathsHelpers",["$rootScope","$log","leafletHelpers",function(a,b,c){function d(a){return a.filter(function(a){return k(a)}).map(function(a){return e(a)})}function e(a){return i(a)?new L.LatLng(a[0],a[1]):new L.LatLng(a.lat,a.lng)}function f(a){return a.map(function(a){return d(a)})}function g(a,b){for(var c={},d=0;d0&&e(a[0].boundingbox)?i.resolve(a[0]):i.reject("[Nominatim] Invalid address")}),i.promise}}}]),angular.module("leaflet-directive").directive("bounds",["$log","$timeout","$http","leafletHelpers","nominatimService","leafletBoundsHelpers",function(a,b,c,d,e,f){return{restrict:"A",scope:!1,replace:!1,require:["leaflet"],link:function(c,g,h,i){var j=d.isDefined,k=f.createLeafletBounds,l=i[0].getLeafletScope(),m=i[0],n=d.errorHeader+" [Bounds] ",o=function(a){return 0===a._southWest.lat&&0===a._southWest.lng&&0===a._northEast.lat&&0===a._northEast.lng};m.getMap().then(function(d){l.$on("boundsChanged",function(a){var c=a.currentScope,e=d.getBounds();if(!o(e)&&!c.settingBoundsFromScope){c.settingBoundsFromLeaflet=!0;var f={northEast:{lat:e._northEast.lat,lng:e._northEast.lng},southWest:{lat:e._southWest.lat,lng:e._southWest.lng},options:e.options};angular.equals(c.bounds,f)||(c.bounds=f),b(function(){c.settingBoundsFromLeaflet=!1})}});var f;l.$watch("bounds",function(g){if(!c.settingBoundsFromLeaflet){if(j(g.address)&&g.address!==f)return c.settingBoundsFromScope=!0,e.query(g.address,h.id).then(function(a){var b=a.boundingbox,c=[[b[0],b[2]],[b[1],b[3]]];d.fitBounds(c)},function(b){a.error(n+" "+b+".")}),f=g.address,void b(function(){c.settingBoundsFromScope=!1});var i=k(g);i&&!d.getBounds().equals(i)&&(c.settingBoundsFromScope=!0,d.fitBounds(i,g.options),b(function(){c.settingBoundsFromScope=!1}))}},!0)})}}}]);var centerDirectiveTypes=["center","lfCenter"],centerDirectives={};centerDirectiveTypes.forEach(function(a){centerDirectives[a]=["$log","$q","$location","$timeout","leafletMapDefaults","leafletHelpers","leafletBoundsHelpers","leafletMapEvents",function(b,c,d,e,f,g,h,i){var j,k=g.isDefined,l=g.isNumber,m=g.isSameCenterOnMap,n=g.safeApply,o=g.isValidCenter,p=h.isValidBounds,q=g.isUndefinedOrEmpty,r=g.errorHeader,s=function(a,b){return k(a)&&p(a)&&q(b)};return{restrict:"A",scope:!1,replace:!1,require:"leaflet",controller:function(){j=c.defer(),this.getCenter=function(){return j.promise}},link:function(c,g,p,q){var t=q.getLeafletScope(),u=t[a];q.getMap().then(function(c){var g=f.getDefaults(p.id);if(-1!==p[a].search("-"))return b.error(r+' The "center" variable can\'t use a "-" on its key name: "'+p[a]+'".'),void c.setView([g.center.lat,g.center.lng],g.center.zoom);if(s(t.bounds,u))c.fitBounds(h.createLeafletBounds(t.bounds),t.bounds.options),u=c.getCenter(),n(t,function(b){angular.extend(b[a],{lat:c.getCenter().lat,lng:c.getCenter().lng,zoom:c.getZoom(),autoDiscover:!1})}),n(t,function(a){var b=c.getBounds();a.bounds={northEast:{lat:b._northEast.lat,lng:b._northEast.lng},southWest:{lat:b._southWest.lat,lng:b._southWest.lng}}});else{if(!k(u))return b.error(r+' The "center" property is not defined in the main scope'),void c.setView([g.center.lat,g.center.lng],g.center.zoom);k(u.lat)&&k(u.lng)||k(u.autoDiscover)||angular.copy(g.center,u)}var q,v;if("yes"===p.urlHashCenter){var w=function(){var a,b=d.search();if(k(b.c)){var c=b.c.split(":");3===c.length&&(a={lat:parseFloat(c[0]),lng:parseFloat(c[1]),zoom:parseInt(c[2],10)})}return a};q=w(),t.$on("$locationChangeSuccess",function(b){var d=b.currentScope,e=w();k(e)&&!m(e,c)&&angular.extend(d[a],{lat:e.lat,lng:e.lng,zoom:e.zoom})})}t.$watch(a,function(a){return t.settingCenterFromLeaflet?void 0:(k(q)&&(angular.copy(q,a),q=void 0),o(a)||a.autoDiscover===!0?a.autoDiscover===!0?(l(a.zoom)||c.setView([g.center.lat,g.center.lng],g.center.zoom),void(l(a.zoom)&&a.zoom>g.center.zoom?c.locate({setView:!0,maxZoom:a.zoom}):k(g.maxZoom)?c.locate({setView:!0,maxZoom:g.maxZoom}):c.locate({setView:!0}))):void(v&&m(a,c)||(t.settingCenterFromScope=!0,c.setView([a.lat,a.lng],a.zoom),i.notifyCenterChangedToBounds(t,c),e(function(){t.settingCenterFromScope=!1}))):void b.warn(r+" invalid 'center'"))},!0),c.whenReady(function(){v=!0}),c.on("moveend",function(){j.resolve(),i.notifyCenterUrlHashChanged(t,c,p,d.search()),m(u,c)||t.settingCenterFromScope||(t.settingCenterFromLeaflet=!0,n(t,function(b){t.settingCenterFromScope||angular.extend(b[a],{lat:c.getCenter().lat,lng:c.getCenter().lng,zoom:c.getZoom(),autoDiscover:!1}),i.notifyCenterChangedToBounds(t,c),e(function(){t.settingCenterFromLeaflet=!1})}))}),u.autoDiscover===!0&&c.on("locationerror",function(){b.warn(r+" The Geolocation API is unauthorized on this page."),o(u)?(c.setView([u.lat,u.lng],u.zoom),i.notifyCenterChangedToBounds(t,c)):(c.setView([g.center.lat,g.center.lng],g.center.zoom),i.notifyCenterChangedToBounds(t,c))})})}}}]}),centerDirectiveTypes.forEach(function(a){angular.module("leaflet-directive").directive(a,centerDirectives[a])}),angular.module("leaflet-directive").directive("controls",["$log","leafletHelpers","leafletControlHelpers",function(a,b,c){return{restrict:"A",scope:!1,replace:!1,require:"?^leaflet",link:function(d,e,f,g){if(g){var h=c.createControl,i=c.isValidControlType,j=g.getLeafletScope(),k=b.isDefined,l=b.isArray,m={},n=b.errorHeader+" [Controls] ";g.getMap().then(function(b){j.$watchCollection("controls",function(c){for(var d in m)k(c[d])||(b.hasControl(m[d])&&b.removeControl(m[d]),delete m[d]);for(var e in c){var f,g=k(c[e].type)?c[e].type:e;if(!i(g))return void a.error(n+" Invalid control type: "+g+".");if("custom"!==g)f=h(g,c[e]),b.addControl(f),m[e]=f;else{var j=c[e];if(l(j))for(var o in j){var p=j[o];b.addControl(p),m[e]=k(m[e])?m[e].concat([p]):[p]}else b.addControl(j),m[e]=j}}})})}}}}]),angular.module("leaflet-directive").directive("decorations",["$log","leafletHelpers",function(a,b){return{restrict:"A",scope:!1,replace:!1,require:"leaflet",link:function(c,d,e,f){function g(b){return k(b)&&k(b.coordinates)&&(j.isLoaded()||a.error("[AngularJS - Leaflet] The PolylineDecorator Plugin is not loaded.")),L.polylineDecorator(b.coordinates)}function h(a,b){return k(a)&&k(b)&&k(b.coordinates)&&k(b.patterns)?(a.setPaths(b.coordinates),a.setPatterns(b.patterns),a):void 0}var i=f.getLeafletScope(),j=b.PolylineDecoratorPlugin,k=b.isDefined,l={};f.getMap().then(function(a){i.$watch("decorations",function(b){for(var c in l)k(b[c])&&angular.equals(b[c],l)||(a.removeLayer(l[c]),delete l[c]);for(var d in b){var e=b[d],f=g(e);k(f)&&(l[d]=f,a.addLayer(f),h(f,e))}},!0)})}}}]),angular.module("leaflet-directive").directive("eventBroadcast",["$log","$rootScope","leafletHelpers","leafletMapEvents","leafletIterators",function(a,b,c,d,e){return{restrict:"A",scope:!1,replace:!1,require:"leaflet",link:function(b,f,g,h){var i=c.isObject,j=c.isDefined,k=h.getLeafletScope(),l=k.eventBroadcast,m=d.getAvailableMapEvents(),n=d.addEvents;h.getMap().then(function(b){var c=[],d="broadcast";j(l.map)?i(l.map)?("emit"!==l.map.logic&&"broadcast"!==l.map.logic?a.warn("[AngularJS - Leaflet] Available event propagation logic are: 'emit' or 'broadcast'."):d=l.map.logic,i(l.map.enable)&&l.map.enable.length>=0?e.each(l.map.enable,function(a){-1===c.indexOf(a)&&-1!==m.indexOf(a)&&c.push(a)}):a.warn("[AngularJS - Leaflet] event-broadcast.map.enable must be an object check your model.")):a.warn("[AngularJS - Leaflet] event-broadcast.map must be an object check your model."):c=m,n(b,c,"eventName",k,d)})}}}]),angular.module("leaflet-directive").directive("geojson",["$log","$rootScope","leafletData","leafletHelpers","leafletWatchHelpers","leafletDirectiveControlsHelpers","leafletIterators","leafletGeoJsonEvents",function(a,b,c,d,e,f,g,h){var i=e.maybeWatch,j=d.watchOptions,k=f.extend,l=d,m=g;return{restrict:"A",scope:!1,replace:!1,require:"leaflet",link:function(a,b,e,f){var g=d.isDefined,n=f.getLeafletScope(),o={},p=!1;f.getMap().then(function(a){var b=n.geojsonWatchOptions||j,f=function(a,b){var c;return c=angular.isFunction(a.onEachFeature)?a.onEachFeature:function(c,f){d.LabelPlugin.isLoaded()&&g(c.properties.description)&&f.bindLabel(c.properties.description),h.bindEvents(e.id,f,null,c,n,b,{resetStyleOnMouseout:a.resetStyleOnMouseout,mapId:e.id})}},q=l.isDefined(e.geojsonNested)&&l.isTruthy(e.geojsonNested),r=function(){if(o){var b=function(b){g(b)&&a.hasLayer(b)&&a.removeLayer(b)};return q?void m.each(o,function(a){b(a)}):void b(o)}},s=function(b,d){var h=angular.copy(b);if(g(h)&&g(h.data)){var i=f(h,d);g(h.options)||(h.options={style:h.style,filter:h.filter,onEachFeature:i,pointToLayer:h.pointToLayer});var j=L.geoJson(h.data,h.options);d&&l.isString(d)?o[d]=j:o=j,j.addTo(a),p||(p=!0,c.setGeoJSON(o,e.id))}},t=function(a){if(r(),q){if(!a||!Object.keys(a).length)return;return void m.each(a,function(a,b){s(a,b)})}s(a)};k(e.id,"geojson",t,r),i(n,"geojson",b,function(a){t(a)})})}}}]),angular.module("leaflet-directive").directive("layercontrol",["$filter","$log","leafletData","leafletHelpers",function(a,b,c,d){return{restrict:"E",scope:{icons:"=?",autoHideOpacity:"=?",showGroups:"=?",title:"@",baseTitle:"@",overlaysTitle:"@"},replace:!0,transclude:!1,require:"^leaflet",controller:["$scope","$element","$sce",function(a,e,f){b.debug("[Angular Directive - Layers] layers",a,e);var g=d.safeApply,h=d.isDefined;angular.extend(a,{baselayer:"",oldGroup:"",layerProperties:{},groupProperties:{},rangeIsSupported:d.rangeIsSupported(),changeBaseLayer:function(b,e){d.safeApply(a,function(d){d.baselayer=b,c.getMap().then(function(e){c.getLayers().then(function(c){if(!e.hasLayer(c.baselayers[b])){for(var f in d.layers.baselayers)d.layers.baselayers[f].icon=d.icons.unradio,e.hasLayer(c.baselayers[f])&&e.removeLayer(c.baselayers[f]);e.addLayer(c.baselayers[b]),d.layers.baselayers[b].icon=a.icons.radio}})})}),e.preventDefault()},moveLayer:function(b,c,d){var e=Object.keys(a.layers.baselayers).length;if(c>=1+e&&c<=a.overlaysArray.length+e){var f;for(var h in a.layers.overlays)if(a.layers.overlays[h].index===c){f=a.layers.overlays[h];break}f&&g(a,function(){f.index=b.index,b.index=c})}d.stopPropagation(),d.preventDefault()},initIndex:function(b,c){var d=Object.keys(a.layers.baselayers).length;b.index=h(b.index)?b.index:c+d+1},initGroup:function(b){a.groupProperties[b]=a.groupProperties[b]?a.groupProperties[b]:{}},toggleOpacity:function(b,c){if(c.visible){if(a.autoHideOpacity&&!a.layerProperties[c.name].opacityControl)for(var d in a.layerProperties)a.layerProperties[d].opacityControl=!1;a.layerProperties[c.name].opacityControl=!a.layerProperties[c.name].opacityControl}b.stopPropagation(),b.preventDefault()},toggleLegend:function(b){a.layerProperties[b.name].showLegend=!a.layerProperties[b.name].showLegend},showLegend:function(b){return b.legend&&a.layerProperties[b.name].showLegend},unsafeHTML:function(a){return f.trustAsHtml(a)},getOpacityIcon:function(b){return b.visible&&a.layerProperties[b.name].opacityControl?a.icons.close:a.icons.open},getGroupIcon:function(b){return b.visible?a.icons.check:a.icons.uncheck},changeOpacity:function(b){var d=a.layerProperties[b.name].opacity;c.getMap().then(function(e){c.getLayers().then(function(c){var f;for(var g in a.layers.overlays)if(a.layers.overlays[g]===b){f=c.overlays[g];break}e.hasLayer(f)&&(f.setOpacity&&f.setOpacity(d/100),f.getLayers&&f.eachLayer&&f.eachLayer(function(a){a.setOpacity&&a.setOpacity(d/100)}))})})},changeGroupVisibility:function(b){if(h(a.groupProperties[b])){var c=a.groupProperties[b].visible;for(var d in a.layers.overlays){var e=a.layers.overlays[d];e.group===b&&(e.visible=c)}}}});var i=e.get(0);L.Browser.touch?L.DomEvent.on(i,"click",L.DomEvent.stopPropagation):(L.DomEvent.disableClickPropagation(i),L.DomEvent.on(i,"mousewheel",L.DomEvent.stopPropagation))}],template:'

        {{ title }}

        {{ baseTitle }}
        {{ overlaysTitle }}
        Range is not supported in this browser
        ',link:function(a,b,e,f){var g=d.isDefined,h=f.getLeafletScope(),i=h.layers;a.$watch("icons",function(){var b={uncheck:"fa fa-square-o",check:"fa fa-check-square-o",radio:"fa fa-dot-circle-o",unradio:"fa fa-circle-o",up:"fa fa-angle-up",down:"fa fa-angle-down",open:"fa fa-angle-double-down",close:"fa fa-angle-double-up",toggleLegend:"fa fa-pencil-square-o"};g(a.icons)?(angular.extend(b,a.icons),angular.extend(a.icons,b)):a.icons=b}),e.order=!g(e.order)||"normal"!==e.order&&"reverse"!==e.order?"normal":e.order,a.order="normal"===e.order,a.orderNumber="normal"===e.order?-1:1,a.layers=i,f.getMap().then(function(b){h.$watch("layers.baselayers",function(d){var e={};c.getLayers().then(function(c){var f;for(f in d){var g=d[f];g.icon=a.icons[b.hasLayer(c.baselayers[f])?"radio":"unradio"],e[f]=g}a.baselayersArray=e})}),h.$watch("layers.overlays",function(b){var d=[],e={};c.getLayers().then(function(c){var f;for(f in b){var h=b[f];h.icon=a.icons[h.visible?"check":"uncheck"],d.push(h),g(a.layerProperties[h.name])||(a.layerProperties[h.name]={opacity:g(h.layerOptions.opacity)?100*h.layerOptions.opacity:100,opacityControl:!1,showLegend:!0}),g(h.group)&&(g(a.groupProperties[h.group])||(a.groupProperties[h.group]={visible:!1}),e[h.group]=g(e[h.group])?e[h.group]:{count:0,visibles:0},e[h.group].count++,h.visible&&e[h.group].visibles++),g(h.index)&&c.overlays[f].setZIndex&&c.overlays[f].setZIndex(b[f].index)}for(f in e)a.groupProperties[f].visible=e[f].visibles===e[f].count;a.overlaysArray=d})},!0)})}}}]),angular.module("leaflet-directive").directive("layers",["$log","$q","leafletData","leafletHelpers","leafletLayerHelpers","leafletControlHelpers",function(a,b,c,d,e,f){return{restrict:"A",scope:!1,replace:!1,require:"leaflet",controller:["$scope",function(a){a._leafletLayers=b.defer(),this.getLayers=function(){return a._leafletLayers.promise}}],link:function(a,b,g,h){var i=d.isDefined,j={},k=h.getLeafletScope(),l=k.layers,m=e.createLayer,n=e.safeAddLayer,o=e.safeRemoveLayer,p=f.updateLayersControl,q=!1;h.getMap().then(function(b){a._leafletLayers.resolve(j),c.setLayers(j,g.id),j.baselayers={},j.overlays={};var d=g.id,e=!1;for(var f in l.baselayers){var h=m(l.baselayers[f]);i(h)?(j.baselayers[f]=h,l.baselayers[f].top===!0&&(n(b,j.baselayers[f]),e=!0)):delete l.baselayers[f]}!e&&Object.keys(j.baselayers).length>0&&n(b,j.baselayers[Object.keys(l.baselayers)[0]]);for(f in l.overlays){var r=m(l.overlays[f]);i(r)?(j.overlays[f]=r,l.overlays[f].visible===!0&&n(b,j.overlays[f])):delete l.overlays[f]}k.$watch("layers.baselayers",function(a,c){if(angular.equals(a,c))return q=p(b,d,q,a,l.overlays,j),!0;for(var e in j.baselayers)(!i(a[e])||a[e].doRefresh)&&(b.hasLayer(j.baselayers[e])&&b.removeLayer(j.baselayers[e]),delete j.baselayers[e],a[e]&&a[e].doRefresh&&(a[e].doRefresh=!1));for(var f in a)if(i(j.baselayers[f]))a[f].top!==!0||b.hasLayer(j.baselayers[f])?a[f].top===!1&&b.hasLayer(j.baselayers[f])&&b.removeLayer(j.baselayers[f]):n(b,j.baselayers[f]);else{var g=m(a[f]);i(g)&&(j.baselayers[f]=g,a[f].top===!0&&n(b,j.baselayers[f]))}var h=!1;for(var k in j.baselayers)if(b.hasLayer(j.baselayers[k])){h=!0;break}!h&&Object.keys(j.baselayers).length>0&&n(b,j.baselayers[Object.keys(j.baselayers)[0]]),q=p(b,d,q,a,l.overlays,j)},!0),k.$watch("layers.overlays",function(a,c){if(angular.equals(a,c))return q=p(b,d,q,l.baselayers,a,j),!0;for(var e in j.overlays)if(!i(a[e])||a[e].doRefresh){if(b.hasLayer(j.overlays[e])){var f=i(a[e])?a[e].layerOptions:null;o(b,j.overlays[e],f)}delete j.overlays[e],a[e]&&a[e].doRefresh&&(a[e].doRefresh=!1)}for(var g in a){if(i(j.overlays[g]))a[g].visible&&!b.hasLayer(j.overlays[g])?n(b,j.overlays[g]):a[g].visible===!1&&b.hasLayer(j.overlays[g])&&o(b,j.overlays[g],a[g].layerOptions);else{ +var h=m(a[g]);if(!i(h))continue;j.overlays[g]=h,a[g].visible===!0&&n(b,j.overlays[g])}a[g].visible&&b._loaded&&a[g].data&&"heatmap"===a[g].type&&(j.overlays[g].setData(a[g].data),j.overlays[g].update())}q=p(b,d,q,l.baselayers,a,j)},!0)})}}}]),angular.module("leaflet-directive").directive("legend",["$log","$http","leafletHelpers","leafletLegendHelpers",function(a,b,c,d){return{restrict:"A",scope:!1,replace:!1,require:"leaflet",link:function(e,f,g,h){var i,j,k,l,m=c.isArray,n=c.isDefined,o=c.isFunction,p=h.getLeafletScope(),q=p.legend;p.$watch("legend",function(a){n(a)&&(i=a.legendClass?a.legendClass:"legend",j=a.position||"bottomright",l=a.type||"arcgis")},!0),h.getMap().then(function(c){p.$watch("legend",function(b){return n(b)?n(b.url)||"arcgis"!==l||m(b.colors)&&m(b.labels)&&b.colors.length===b.labels.length?n(b.url)?void a.info("[AngularJS - Leaflet] loading legend service."):(n(k)&&(k.removeFrom(c),k=null),k=L.control({position:j}),"arcgis"===l&&(k.onAdd=d.getOnAddArrayLegend(b,i)),void k.addTo(c)):void a.warn("[AngularJS - Leaflet] legend.colors and legend.labels must be set."):void(n(k)&&(k.removeFrom(c),k=null))}),p.$watch("legend.url",function(e){n(e)&&b.get(e).success(function(a){n(k)?d.updateLegend(k.getContainer(),a,l,e):(k=L.control({position:j}),k.onAdd=d.getOnAddLegend(a,i,l,e),k.addTo(c)),n(q.loadedData)&&o(q.loadedData)&&q.loadedData()}).error(function(){a.warn("[AngularJS - Leaflet] legend.url not loaded.")})})})}}}]),angular.module("leaflet-directive").directive("markers",["$log","$rootScope","$q","leafletData","leafletHelpers","leafletMapDefaults","leafletMarkersHelpers","leafletMarkerEvents","leafletIterators","leafletWatchHelpers","leafletDirectiveControlsHelpers",function(a,b,c,d,e,f,g,h,i,j,k){var l=e.isDefined,m=e.errorHeader,n=e,o=e.isString,p=g.addMarkerWatcher,q=g.updateMarker,r=g.listenMarkerEvents,s=g.addMarkerToGroup,t=g.createMarker,u=g.deleteMarker,v=i,w=e.watchOptions,x=j.maybeWatch,y=k.extend,z=function(a,b,c){if(Object.keys(a).length){if(c&&o(c)){if(!a[c]||!Object.keys(a[c]).length)return;return a[c][b]}return a[b]}},A=function(a,b,c,d){return d&&o(d)?(l(b[d])||(b[d]={}),b[d][c]=a):b[c]=a,a},B=function(b,c,d,e,f,g){if(!o(b))return a.error(m+" A layername must be a string"),!1;if(!l(c))return a.error(m+" You must add layers to the directive if the markers are going to use this functionality."),!1;if(!l(c.overlays)||!l(c.overlays[b]))return a.error(m+' A marker can only be added to a layer of type "group"'),!1;var h=c.overlays[b];return h instanceof L.LayerGroup||h instanceof L.FeatureGroup?(h.addLayer(e),!f&&g.hasLayer(e)&&d.focus===!0&&e.openPopup(),!0):(a.error(m+' Adding a marker to an overlay needs a overlay of the type "group" or "featureGroup"'),!1)},C=function(b,c,d,e,f,g,i,j,k,o){for(var u in c)if(!o[u])if(-1===u.search("-")){var v=n.copy(c[u]),w=n.getObjectDotPath(k?[k,u]:[u]),x=z(g,u,k);if(l(x)){var y=l(y)?d[u]:void 0;q(v,y,x,w,i,f,e)}else{var C=t(v),D=(v?v.layer:void 0)||k;if(!l(C)){a.error(m+" Received invalid data on the marker "+u+".");continue}if(A(C,g,u,k),l(v.message)&&C.bindPopup(v.message,v.popupOptions),l(v.group)){var E=l(v.groupOption)?v.groupOption:null;s(C,v.group,E,e)}if(n.LabelPlugin.isLoaded()&&l(v.label)&&l(v.label.message)&&C.bindLabel(v.label.message,v.label.options),l(v)&&(l(v.layer)||l(k))){var F=B(D,f,v,C,j.individual.doWatch,e);if(!F)continue}else l(v.group)||(e.addLayer(C),j.individual.doWatch||v.focus!==!0||C.openPopup());j.individual.doWatch&&p(C,w,i,f,e,j.individual.isDeep),r(C,v,i,j.individual.doWatch,e),h.bindEvents(b,C,w,v,i,D)}}else a.error('The marker can\'t use a "-" on his key name: "'+u+'".')},D=function(b,c,d,e,f){var g,h,i=!1,j=!1,k=l(c);for(var o in d)i||(a.debug(m+"[markers] destroy: "),i=!0),k&&(h=b[o],g=c[o],j=angular.equals(h,g)&&e),l(b)&&Object.keys(b).length&&l(b[o])&&Object.keys(b[o]).length&&!j||f&&n.isFunction(f)&&f(h,g,o)},E=function(b,c,d,e,f){D(b,c,d,!1,function(b,c,g){a.debug(m+"[marker] is deleting marker: "+g),u(d[g],e,f),delete d[g]})},F=function(b,c,d){var e={};return D(b,c,d,!0,function(b,c,d){a.debug(m+"[marker] is already rendered, marker: "+d),e[d]=b}),e};return{restrict:"A",scope:!1,replace:!1,require:["leaflet","?layers"],link:function(a,b,e,f){var g=f[0],h=g.getLeafletScope();g.getMap().then(function(a){var b,g={};b=l(f[1])?f[1].getLayers:function(){var a=c.defer();return a.resolve(),a.promise};var i=h.markersWatchOptions||w;l(e.watchMarkers)&&(i.doWatch=i.individual.doWatch=!l(e.watchMarkers)||n.isTruthy(e.watchMarkers));var j=l(e.markersNested)&&n.isTruthy(e.markersNested);b().then(function(b){var c=function(c,d){return j?void v.each(c,function(c,e){var f=l(f)?d[e]:void 0;E(c,f,g[e],a,b)}):void E(c,d,g,a,b)},f=function(d,f){c(d,f);var k=null;return j?void v.each(d,function(c,j){var m=l(m)?f[j]:void 0;k=F(d[j],m,g[j]),C(e.id,c,f,a,b,g,h,i,j,k)}):(k=F(d,f,g),void C(e.id,d,f,a,b,g,h,i,void 0,k))};y(e.id,"markers",f,c),d.setMarkers(g,e.id),x(h,"markers",i,function(a,b){f(a,b)})})})}}}]),angular.module("leaflet-directive").directive("maxbounds",["$log","leafletMapDefaults","leafletBoundsHelpers","leafletHelpers",function(a,b,c,d){return{restrict:"A",scope:!1,replace:!1,require:"leaflet",link:function(a,b,e,f){var g=f.getLeafletScope(),h=c.isValidBounds,i=d.isNumber;f.getMap().then(function(a){g.$watch("maxbounds",function(b){if(!h(b))return void a.setMaxBounds();var d=c.createLeafletBounds(b);i(b.pad)&&(d=d.pad(b.pad)),a.setMaxBounds(d),e.center||e.lfCenter||a.fitBounds(d)})})}}}]),angular.module("leaflet-directive").directive("paths",["$log","$q","leafletData","leafletMapDefaults","leafletHelpers","leafletPathsHelpers","leafletPathEvents",function(a,b,c,d,e,f,g){return{restrict:"A",scope:!1,replace:!1,require:["leaflet","?layers"],link:function(h,i,j,k){var l=k[0],m=e.isDefined,n=e.isString,o=l.getLeafletScope(),p=o.paths,q=f.createPath,r=g.bindPathEvents,s=f.setPathOptions;l.getMap().then(function(f){var g,h=d.getDefaults(j.id);g=m(k[1])?k[1].getLayers:function(){var a=b.defer();return a.resolve(),a.promise},m(p)&&g().then(function(b){var d={};c.setPaths(d,j.id);var g=!m(j.watchPaths)||"true"===j.watchPaths,i=function(a,c){var d=o.$watch('paths["'+c+'"]',function(c,e){if(!m(c)){if(m(e.layer))for(var g in b.overlays){var h=b.overlays[g];h.removeLayer(a)}return f.removeLayer(a),void d()}s(a,c.type,c)},!0)};o.$watchCollection("paths",function(c){for(var k in d)m(c[k])||(f.removeLayer(d[k]),delete d[k]);for(var l in c)if(0!==l.search("\\$"))if(-1===l.search("-")){if(!m(d[l])){var p=c[l],t=q(l,c[l],h);if(m(t)&&m(p.message)&&t.bindPopup(p.message,p.popupOptions),e.LabelPlugin.isLoaded()&&m(p.label)&&m(p.label.message)&&t.bindLabel(p.label.message,p.label.options),m(p)&&m(p.layer)){if(!n(p.layer)){a.error("[AngularJS - Leaflet] A layername must be a string");continue}if(!m(b)){a.error("[AngularJS - Leaflet] You must add layers to the directive if the markers are going to use this functionality.");continue}if(!m(b.overlays)||!m(b.overlays[p.layer])){a.error('[AngularJS - Leaflet] A path can only be added to a layer of type "group"');continue}var u=b.overlays[p.layer];if(!(u instanceof L.LayerGroup||u instanceof L.FeatureGroup)){a.error('[AngularJS - Leaflet] Adding a path to an overlay needs a overlay of the type "group" or "featureGroup"');continue}d[l]=t,u.addLayer(t),g?i(t,l):s(t,p.type,p)}else m(t)&&(d[l]=t,f.addLayer(t),g?i(t,l):s(t,p.type,p));r(j.id,t,l,p,o)}}else a.error('[AngularJS - Leaflet] The path name "'+l+'" is not valid. It must not include "-" and a number.')})})})}}}]),angular.module("leaflet-directive").directive("tiles",["$log","leafletData","leafletMapDefaults","leafletHelpers",function(a,b,c,d){return{restrict:"A",scope:!1,replace:!1,require:"leaflet",link:function(e,f,g,h){var i=d.isDefined,j=h.getLeafletScope(),k=j.tiles;return i(k)&&i(k.url)?void h.getMap().then(function(a){var d,e=c.getDefaults(g.id);j.$watch("tiles",function(c,f){var h=e.tileLayerOptions,j=e.tileLayer;return!i(c.url)&&i(d)?void a.removeLayer(d):i(d)?!i(c.url)||!i(c.options)||c.type===f.type&&angular.equals(c.options,h)?void(i(c.url)&&d.setUrl(c.url)):(a.removeLayer(d),h=e.tileLayerOptions,angular.copy(c.options,h),j=c.url,d="wms"===c.type?L.tileLayer.wms(j,h):L.tileLayer(j,h),d.addTo(a),void b.setTiles(d,g.id)):(i(c.options)&&angular.copy(c.options,h),i(c.url)&&(j=c.url),d="wms"===c.type?L.tileLayer.wms(j,h):L.tileLayer(j,h),d.addTo(a),void b.setTiles(d,g.id))},!0)}):void a.warn("[AngularJS - Leaflet] The 'tiles' definition doesn't have the 'url' property.")}}}]),["markers","geojson"].forEach(function(a){angular.module("leaflet-directive").directive(a+"WatchOptions",["$log","$rootScope","$q","leafletData","leafletHelpers",function(b,c,d,e,f){var g=f.isDefined,h=f.errorHeader,i=f.isObject,j=f.watchOptions;return{restrict:"A",scope:!1,replace:!1,require:["leaflet"],link:function(c,d,e,f){var k=f[0],l=k.getLeafletScope();k.getMap().then(function(){g(c[a+"WatchOptions"])&&(i(c[a+"WatchOptions"])?angular.extend(j,c[a+"WatchOptions"]):b.error(h+"["+a+"WatchOptions] is not an object"),l[a+"WatchOptions"]=j)})}}}])}),angular.module("leaflet-directive").factory("LeafletEventsHelpersFactory",["$rootScope","$q","$log","leafletHelpers",function(a,b,c,d){var e=d.safeApply,f=d.isDefined,g=d.isObject,h=d.isArray,i=d.errorHeader,j=function(a,b){this.rootBroadcastName=a,c.debug("LeafletEventsHelpersFactory: lObjectType: "+b+"rootBroadcastName: "+a),this.lObjectType=b};return j.prototype.getAvailableEvents=function(){return[]},j.prototype.genDispatchEvent=function(a,b,d,e,f,g,h,i,j){var k=this;return a=a||"",a&&(a="."+a),function(l){var m=k.rootBroadcastName+a+"."+b;c.debug(m),k.fire(e,m,d,l,l.target||f,h,g,i,j)}},j.prototype.fire=function(b,c,d,g,h,i,j,k){e(b,function(){var e={leafletEvent:g,leafletObject:h,modelName:j,model:i};f(k)&&angular.extend(e,{layerName:k}),"emit"===d?b.$emit(c,e):a.$broadcast(c,e)})},j.prototype.bindEvents=function(a,b,d,e,j,k,l){var m=[],n="emit",o=this;if(f(j.eventBroadcast))if(g(j.eventBroadcast))if(f(j.eventBroadcast[o.lObjectType]))if(g(j.eventBroadcast[o.lObjectType])){f(j.eventBroadcast[this.lObjectType].logic)&&"emit"!==j.eventBroadcast[o.lObjectType].logic&&"broadcast"!==j.eventBroadcast[o.lObjectType].logic&&c.warn(i+"Available event propagation logic are: 'emit' or 'broadcast'.");var p=!1,q=!1;f(j.eventBroadcast[o.lObjectType].enable)&&h(j.eventBroadcast[o.lObjectType].enable)&&(p=!0),f(j.eventBroadcast[o.lObjectType].disable)&&h(j.eventBroadcast[o.lObjectType].disable)&&(q=!0),p&&q?c.warn(i+"can not enable and disable events at the same time"):p||q?p?j.eventBroadcast[this.lObjectType].enable.forEach(function(a){-1!==m.indexOf(a)?c.warn(i+"This event "+a+" is already enabled"):-1===o.getAvailableEvents().indexOf(a)?c.warn(i+"This event "+a+" does not exist"):m.push(a)}):(m=this.getAvailableEvents(),j.eventBroadcast[o.lObjectType].disable.forEach(function(a){var b=m.indexOf(a);-1===b?c.warn(i+"This event "+a+" does not exist or has been already disabled"):m.splice(b,1)})):c.warn(i+"must enable or disable events")}else c.warn(i+"event-broadcast."+[o.lObjectType]+" must be an object check your model.");else m=this.getAvailableEvents();else c.error(i+"event-broadcast must be an object check your model.");else m=this.getAvailableEvents();return m.forEach(function(c){b.on(c,o.genDispatchEvent(a,c,n,j,b,d,e,k,l))}),n},j}]).service("leafletEventsHelpers",["LeafletEventsHelpersFactory",function(a){return new a}]),angular.module("leaflet-directive").factory("leafletGeoJsonEvents",["$rootScope","$q","$log","leafletHelpers","LeafletEventsHelpersFactory","leafletData",function(a,b,c,d,e,f){var g=d.safeApply,h=e,i=function(){h.call(this,"leafletDirectiveGeoJson","geojson")};return i.prototype=new h,i.prototype.genDispatchEvent=function(b,c,d,e,i,j,k,l,m){var n=h.prototype.genDispatchEvent.call(this,b,c,d,e,i,j,k,l),o=this;return function(b){"mouseout"===c&&(m.resetStyleOnMouseout&&f.getGeoJSON(m.mapId).then(function(a){var c=l?a[l]:a;c.resetStyle(b.target)}),g(e,function(){a.$broadcast(o.rootBroadcastName+".mouseout",b)})),n(b)}},i.prototype.getAvailableEvents=function(){return["click","dblclick","mouseover","mouseout"]},new i}]),angular.module("leaflet-directive").factory("leafletLabelEvents",["$rootScope","$q","$log","leafletHelpers","LeafletEventsHelpersFactory",function(a,b,c,d,e){var f=d,g=e,h=function(){g.call(this,"leafletDirectiveLabel","markers")};return h.prototype=new g,h.prototype.genDispatchEvent=function(a,b,c,d,e,f,h,i){var j=f.replace("markers.","");return g.prototype.genDispatchEvent.call(this,a,b,c,d,e,j,h,i)},h.prototype.getAvailableEvents=function(){return["click","dblclick","mousedown","mouseover","mouseout","contextmenu"]},h.prototype.genEvents=function(a,b,c,d,e,g,h,i){var j=this,k=this.getAvailableEvents(),l=f.getObjectArrayPath("markers."+g);k.forEach(function(b){e.label.on(b,j.genDispatchEvent(a,b,c,d,e.label,l,h,i))})},h.prototype.bindEvents=function(){},new h}]),angular.module("leaflet-directive").factory("leafletMapEvents",["$rootScope","$q","$log","leafletHelpers","leafletEventsHelpers","leafletIterators",function(a,b,c,d,e,f){var g=d.isDefined,h=e.fire,i=function(){return["click","dblclick","mousedown","mouseup","mouseover","mouseout","mousemove","contextmenu","focus","blur","preclick","load","unload","viewreset","movestart","move","moveend","dragstart","drag","dragend","zoomstart","zoomanim","zoomend","zoomlevelschange","resize","autopanstart","layeradd","layerremove","baselayerchange","overlayadd","overlayremove","locationfound","locationerror","popupopen","popupclose","draw:created","draw:edited","draw:deleted","draw:drawstart","draw:drawstop","draw:editstart","draw:editstop","draw:deletestart","draw:deletestop"]},j=function(a,b,d,e){return e&&(e+="."),function(f){var g="leafletDirectiveMap."+e+b;c.debug(g),h(a,g,d,f,f.target,a)}},k=function(a){a.$broadcast("boundsChanged")},l=function(a,b,c,d){if(g(c.urlHashCenter)){var e=b.getCenter(),f=e.lat.toFixed(4)+":"+e.lng.toFixed(4)+":"+b.getZoom();g(d.c)&&d.c===f||a.$emit("centerUrlHash",f)}},m=function(a,b,c,d,e){f.each(b,function(b){var f={};f[c]=b,a.on(b,j(d,b,e,a._container.id||""),f)})};return{getAvailableMapEvents:i,genDispatchMapEvent:j,notifyCenterChangedToBounds:k,notifyCenterUrlHashChanged:l,addEvents:m}}]),angular.module("leaflet-directive").factory("leafletMarkerEvents",["$rootScope","$q","$log","leafletHelpers","LeafletEventsHelpersFactory","leafletLabelEvents",function(a,b,c,d,e,f){var g=d.safeApply,h=d.isDefined,i=d,j=f,k=e,l=function(){k.call(this,"leafletDirectiveMarker","markers")};return l.prototype=new k,l.prototype.genDispatchEvent=function(b,c,d,e,f,h,i,j){var l=k.prototype.genDispatchEvent.call(this,b,c,d,e,f,h,i,j);return function(b){"click"===c?g(e,function(){a.$broadcast("leafletDirectiveMarkersClick",h)}):"dragend"===c&&(g(e,function(){i.lat=f.getLatLng().lat,i.lng=f.getLatLng().lng}),i.message&&i.focus===!0&&f.openPopup()),l(b)}},l.prototype.getAvailableEvents=function(){return["click","dblclick","mousedown","mouseover","mouseout","contextmenu","dragstart","drag","dragend","move","remove","popupopen","popupclose","touchend","touchstart","touchmove","touchcancel","touchleave"]},l.prototype.bindEvents=function(a,b,c,d,e,f){var g=k.prototype.bindEvents.call(this,a,b,c,d,e,f);i.LabelPlugin.isLoaded()&&h(b.label)&&j.genEvents(a,c,g,e,b,d,f)},new l}]),angular.module("leaflet-directive").factory("leafletPathEvents",["$rootScope","$q","$log","leafletHelpers","leafletLabelEvents","leafletEventsHelpers",function(a,b,c,d,e,f){var g=d.isDefined,h=d.isObject,i=d,j=d.errorHeader,k=e,l=f.fire,m=function(a,b,d,e,f,g,h,i){return a=a||"",a&&(a="."+a),function(j){var k="leafletDirectivePath"+a+"."+b;c.debug(k),l(e,k,d,j,j.target||f,h,g,i)}},n=function(a,b,d,e,f){var l,n,p=[],q="broadcast";if(g(f.eventBroadcast))if(h(f.eventBroadcast))if(g(f.eventBroadcast.path))if(h(f.eventBroadcast.paths))c.warn(j+"event-broadcast.path must be an object check your model.");else{void 0!==f.eventBroadcast.path.logic&&null!==f.eventBroadcast.path.logic&&("emit"!==f.eventBroadcast.path.logic&&"broadcast"!==f.eventBroadcast.path.logic?c.warn(j+"Available event propagation logic are: 'emit' or 'broadcast'."):"emit"===f.eventBroadcast.path.logic&&(q="emit"));var r=!1,s=!1;if(void 0!==f.eventBroadcast.path.enable&&null!==f.eventBroadcast.path.enable&&"object"==typeof f.eventBroadcast.path.enable&&(r=!0),void 0!==f.eventBroadcast.path.disable&&null!==f.eventBroadcast.path.disable&&"object"==typeof f.eventBroadcast.path.disable&&(s=!0),r&&s)c.warn(j+"can not enable and disable events at the same time");else if(r||s)if(r)for(l=0;l
        ', + controller: ["$scope", function($scope) { + this._leafletMap = $q.defer(); + this.getMap = function() { + return this._leafletMap.promise; + }; + + this.getLeafletScope = function() { + return $scope; + }; + }], + + link: function(scope, element, attrs, ctrl) { + var isDefined = leafletHelpers.isDefined; + var defaults = leafletMapDefaults.setDefaults(scope.defaults, attrs.id); + var mapEvents = leafletMapEvents.getAvailableMapEvents(); + var addEvents = leafletMapEvents.addEvents; + + scope.mapId = attrs.id; + leafletData.setDirectiveControls({}, attrs.id); + + // Set width and height utility functions + function updateWidth() { + if (isNaN(attrs.width)) { + element.css('width', attrs.width); + } else { + element.css('width', attrs.width + 'px'); + } + } + + function updateHeight() { + if (isNaN(attrs.height)) { + element.css('height', attrs.height); + } else { + element.css('height', attrs.height + 'px'); + } + } + + // If the width attribute defined update css + // Then watch if bound property changes and update css + if (isDefined(attrs.width)) { + updateWidth(); + + scope.$watch( + function() { + return element[0].getAttribute('width'); + }, + + function() { + updateWidth(); + map.invalidateSize(); + }); + } + + // If the height attribute defined update css + // Then watch if bound property changes and update css + if (isDefined(attrs.height)) { + updateHeight(); + + scope.$watch( + function() { + return element[0].getAttribute('height'); + }, + + function() { + updateHeight(); + map.invalidateSize(); + }); + } + + // Create the Leaflet Map Object with the options + var map = new L.Map(element[0], leafletMapDefaults.getMapCreationDefaults(attrs.id)); + ctrl._leafletMap.resolve(map); + + if (!isDefined(attrs.center) && !isDefined(attrs.lfCenter)) { + map.setView([defaults.center.lat, defaults.center.lng], defaults.center.zoom); + } + + // If no layers nor tiles defined, set the default tileLayer + if (!isDefined(attrs.tiles) && (!isDefined(attrs.layers))) { + var tileLayerObj = L.tileLayer(defaults.tileLayer, defaults.tileLayerOptions); + tileLayerObj.addTo(map); + leafletData.setTiles(tileLayerObj, attrs.id); + } + + // Set zoom control configuration + if (isDefined(map.zoomControl) && + isDefined(defaults.zoomControlPosition)) { + map.zoomControl.setPosition(defaults.zoomControlPosition); + } + + if (isDefined(map.zoomControl) && defaults.zoomControl === false) { + map.zoomControl.removeFrom(map); + } + + if (isDefined(map.zoomsliderControl) && + isDefined(defaults.zoomsliderControl) && + defaults.zoomsliderControl === false) { + map.zoomsliderControl.removeFrom(map); + } + + // if no event-broadcast attribute, all events are broadcasted + if (!isDefined(attrs.eventBroadcast)) { + var logic = 'broadcast'; + addEvents(map, mapEvents, 'eventName', scope, logic); + } + + // Resolve the map object to the promises + map.whenReady(function() { + leafletData.setMap(map, attrs.id); + }); + + scope.$on('$destroy', function() { + leafletMapDefaults.reset(); + map.remove(); + leafletData.unresolveMap(attrs.id); + }); + + //Handle request to invalidate the map size + //Up scope using $scope.$emit('invalidateSize') + //Down scope using $scope.$broadcast('invalidateSize') + scope.$on('invalidateSize', function() { + map.invalidateSize(); + }); + }, + }; +}]); + +angular.module('leaflet-directive').factory('leafletBoundsHelpers', ["$log", "leafletHelpers", function($log, leafletHelpers) { + + var isArray = leafletHelpers.isArray; + var isNumber = leafletHelpers.isNumber; + var isFunction = leafletHelpers.isFunction; + var isDefined = leafletHelpers.isDefined; + + function _isValidBounds(bounds) { + return angular.isDefined(bounds) && angular.isDefined(bounds.southWest) && + angular.isDefined(bounds.northEast) && angular.isNumber(bounds.southWest.lat) && + angular.isNumber(bounds.southWest.lng) && angular.isNumber(bounds.northEast.lat) && + angular.isNumber(bounds.northEast.lng); + } + + return { + createLeafletBounds: function(bounds) { + if (_isValidBounds(bounds)) { + return L.latLngBounds([bounds.southWest.lat, bounds.southWest.lng], + [bounds.northEast.lat, bounds.northEast.lng]); + } + }, + + isValidBounds: _isValidBounds, + + createBoundsFromArray: function(boundsArray) { + if (!(isArray(boundsArray) && boundsArray.length === 2 && + isArray(boundsArray[0]) && isArray(boundsArray[1]) && + boundsArray[0].length === 2 && boundsArray[1].length === 2 && + isNumber(boundsArray[0][0]) && isNumber(boundsArray[0][1]) && + isNumber(boundsArray[1][0]) && isNumber(boundsArray[1][1]))) { + $log.error('[AngularJS - Leaflet] The bounds array is not valid.'); + return; + } + + return { + northEast: { + lat: boundsArray[0][0], + lng: boundsArray[0][1], + }, + southWest: { + lat: boundsArray[1][0], + lng: boundsArray[1][1], + }, + }; + }, + + createBoundsFromLeaflet: function(lfBounds) { + if (!(isDefined(lfBounds) && isFunction(lfBounds.getNorthEast) && isFunction(lfBounds.getSouthWest))) { + $log.error('[AngularJS - Leaflet] The leaflet bounds is not valid object.'); + return; + } + + var northEast = lfBounds.getNorthEast(); + var southWest = lfBounds.getSouthWest(); + + return { + northEast: { + lat: northEast.lat, + lng: northEast.lng, + }, + southWest: { + lat: southWest.lat, + lng: southWest.lng, + }, + }; + }, + }; +}]); + +angular.module('leaflet-directive').factory('leafletControlHelpers', ["$rootScope", "$log", "leafletHelpers", "leafletLayerHelpers", "leafletMapDefaults", function($rootScope, $log, leafletHelpers, leafletLayerHelpers, leafletMapDefaults) { + var isDefined = leafletHelpers.isDefined; + var isObject = leafletHelpers.isObject; + var createLayer = leafletLayerHelpers.createLayer; + var _controls = {}; + var errorHeader = leafletHelpers.errorHeader + ' [Controls] '; + + var _controlLayersMustBeVisible = function(baselayers, overlays, mapId) { + var defaults = leafletMapDefaults.getDefaults(mapId); + if (!defaults.controls.layers.visible) { + return false; + } + + var atLeastOneControlItemMustBeShown = false; + + if (isObject(baselayers)) { + Object.keys(baselayers).forEach(function(key) { + var layer = baselayers[key]; + if (!isDefined(layer.layerOptions) || layer.layerOptions.showOnSelector !== false) { + atLeastOneControlItemMustBeShown = true; + } + }); + } + + if (isObject(overlays)) { + Object.keys(overlays).forEach(function(key) { + var layer = overlays[key]; + if (!isDefined(layer.layerParams) || layer.layerParams.showOnSelector !== false) { + atLeastOneControlItemMustBeShown = true; + } + }); + } + + return atLeastOneControlItemMustBeShown; + }; + + var _createLayersControl = function(mapId) { + var defaults = leafletMapDefaults.getDefaults(mapId); + var controlOptions = { + collapsed: defaults.controls.layers.collapsed, + position: defaults.controls.layers.position, + autoZIndex: false, + }; + + angular.extend(controlOptions, defaults.controls.layers.options); + + var control; + if (defaults.controls.layers && isDefined(defaults.controls.layers.control)) { + control = defaults.controls.layers.control.apply(this, [[], [], controlOptions]); + } else { + control = new L.control.layers([], [], controlOptions); + } + + return control; + }; + + var controlTypes = { + draw: { + isPluginLoaded: function() { + if (!angular.isDefined(L.Control.Draw)) { + $log.error(errorHeader + ' Draw plugin is not loaded.'); + return false; + } + + return true; + }, + + checkValidParams: function(/* params */) { + return true; + }, + + createControl: function(params) { + return new L.Control.Draw(params); + }, + }, + scale: { + isPluginLoaded: function() { + return true; + }, + + checkValidParams: function(/* params */) { + return true; + }, + + createControl: function(params) { + return new L.control.scale(params); + }, + }, + fullscreen: { + isPluginLoaded: function() { + if (!angular.isDefined(L.Control.Fullscreen)) { + $log.error(errorHeader + ' Fullscreen plugin is not loaded.'); + return false; + } + + return true; + }, + + checkValidParams: function(/* params */) { + return true; + }, + + createControl: function(params) { + return new L.Control.Fullscreen(params); + }, + }, + search: { + isPluginLoaded: function() { + if (!angular.isDefined(L.Control.Search)) { + $log.error(errorHeader + ' Search plugin is not loaded.'); + return false; + } + + return true; + }, + + checkValidParams: function(/* params */) { + return true; + }, + + createControl: function(params) { + return new L.Control.Search(params); + }, + }, + custom: {}, + minimap: { + isPluginLoaded: function() { + if (!angular.isDefined(L.Control.MiniMap)) { + $log.error(errorHeader + ' Minimap plugin is not loaded.'); + return false; + } + + return true; + }, + + checkValidParams: function(params) { + if (!isDefined(params.layer)) { + $log.warn(errorHeader + ' minimap "layer" option should be defined.'); + return false; + } + + return true; + }, + + createControl: function(params) { + var layer = createLayer(params.layer); + + if (!isDefined(layer)) { + $log.warn(errorHeader + ' minimap control "layer" could not be created.'); + return; + } + + return new L.Control.MiniMap(layer, params); + }, + }, + }; + + return { + layersControlMustBeVisible: _controlLayersMustBeVisible, + + isValidControlType: function(type) { + return Object.keys(controlTypes).indexOf(type) !== -1; + }, + + createControl: function(type, params) { + if (!controlTypes[type].checkValidParams(params)) { + return; + } + + return controlTypes[type].createControl(params); + }, + + updateLayersControl: function(map, mapId, loaded, baselayers, overlays, leafletLayers) { + var i; + var _layersControl = _controls[mapId]; + var mustBeLoaded = _controlLayersMustBeVisible(baselayers, overlays, mapId); + + if (isDefined(_layersControl) && loaded) { + for (i in leafletLayers.baselayers) { + _layersControl.removeLayer(leafletLayers.baselayers[i]); + } + + for (i in leafletLayers.overlays) { + _layersControl.removeLayer(leafletLayers.overlays[i]); + } + + map.removeControl(_layersControl); + delete _controls[mapId]; + } + + if (mustBeLoaded) { + _layersControl = _createLayersControl(mapId); + _controls[mapId] = _layersControl; + for (i in baselayers) { + var hideOnSelector = isDefined(baselayers[i].layerOptions) && + baselayers[i].layerOptions.showOnSelector === false; + if (!hideOnSelector && isDefined(leafletLayers.baselayers[i])) { + _layersControl.addBaseLayer(leafletLayers.baselayers[i], baselayers[i].name); + } + } + + for (i in overlays) { + var hideOverlayOnSelector = isDefined(overlays[i].layerParams) && + overlays[i].layerParams.showOnSelector === false; + if (!hideOverlayOnSelector && isDefined(leafletLayers.overlays[i])) { + _layersControl.addOverlay(leafletLayers.overlays[i], overlays[i].name); + } + } + + map.addControl(_layersControl); + } + + return mustBeLoaded; + }, + }; +}]); + +angular.module('leaflet-directive').service('leafletData', ["$log", "$q", "leafletHelpers", function($log, $q, leafletHelpers) { + var getDefer = leafletHelpers.getDefer, + getUnresolvedDefer = leafletHelpers.getUnresolvedDefer, + setResolvedDefer = leafletHelpers.setResolvedDefer; + + var _private = {}; + var self = this; + + var upperFirst = function(string) { + return string.charAt(0).toUpperCase() + string.slice(1); + }; + + var _privateItems = [ + 'map', + 'tiles', + 'layers', + 'paths', + 'markers', + 'geoJSON', + 'UTFGrid', //odd ball on naming convention keeping to not break + 'decorations', + 'directiveControls',]; + + //init + _privateItems.forEach(function(itemName) { + _private[itemName] = {}; + }); + + this.unresolveMap = function(scopeId) { + var id = leafletHelpers.obtainEffectiveMapId(_private.map, scopeId); + _privateItems.forEach(function(itemName) { + _private[itemName][id] = undefined; + }); + }; + + //int repetitive stuff (get and sets) + _privateItems.forEach(function(itemName) { + var name = upperFirst(itemName); + self['set' + name] = function(lObject, scopeId) { + var defer = getUnresolvedDefer(_private[itemName], scopeId); + defer.resolve(lObject); + setResolvedDefer(_private[itemName], scopeId); + }; + + self['get' + name] = function(scopeId) { + var defer = getDefer(_private[itemName], scopeId); + return defer.promise; + }; + }); +}]); + +angular.module('leaflet-directive') +.service('leafletDirectiveControlsHelpers', ["$log", "leafletData", "leafletHelpers", function($log, leafletData, leafletHelpers) { + var _isDefined = leafletHelpers.isDefined; + var _isString = leafletHelpers.isString; + var _isObject = leafletHelpers.isObject; + var _mainErrorHeader = leafletHelpers.errorHeader; + + var _errorHeader = _mainErrorHeader + '[leafletDirectiveControlsHelpers'; + + var _extend = function(id, thingToAddName, createFn, cleanFn) { + var _fnHeader = _errorHeader + '.extend] '; + var extender = {}; + if (!_isDefined(thingToAddName)) { + $log.error(_fnHeader + 'thingToAddName cannot be undefined'); + return; + } + + if (_isString(thingToAddName) && _isDefined(createFn) && _isDefined(cleanFn)) { + extender[thingToAddName] = { + create: createFn, + clean: cleanFn, + }; + } else if (_isObject(thingToAddName) && !_isDefined(createFn) && !_isDefined(cleanFn)) { + extender = thingToAddName; + } else { + $log.error(_fnHeader + 'incorrect arguments'); + return; + } + + //add external control to create / destroy markers without a watch + leafletData.getDirectiveControls().then(function(controls) { + angular.extend(controls, extender); + leafletData.setDirectiveControls(controls, id); + }); + }; + + return { + extend: _extend, + }; +}]); + +angular.module('leaflet-directive') +.service('leafletGeoJsonHelpers', ["leafletHelpers", "leafletIterators", function(leafletHelpers, leafletIterators) { + var lHlp = leafletHelpers; + var lIt = leafletIterators; + var Point = function(lat, lng) { + this.lat = lat; + this.lng = lng; + return this; + }; + + var _getLat = function(value) { + if (Array.isArray(value) && value.length === 2) { + return value[1]; + } else if (lHlp.isDefined(value.type) && value.type === 'Point') { + return +value.coordinates[1]; + } else { + return +value.lat; + } + }; + + var _getLng = function(value) { + if (Array.isArray(value) && value.length === 2) { + return value[0]; + } else if (lHlp.isDefined(value.type) && value.type === 'Point') { + return +value.coordinates[0]; + } else { + return +value.lng; + } + }; + + var _validateCoords = function(coords) { + if (lHlp.isUndefined(coords)) { + return false; + } + + if (lHlp.isArray(coords)) { + if (coords.length === 2 && lHlp.isNumber(coords[0]) && lHlp.isNumber(coords[1])) { + return true; + } + } else if (lHlp.isDefined(coords.type)) { + if ( + coords.type === 'Point' && lHlp.isArray(coords.coordinates) && + coords.coordinates.length === 2 && + lHlp.isNumber(coords.coordinates[0]) && + lHlp.isNumber(coords.coordinates[1])) { + return true; + } + } + + var ret = lIt.all(['lat', 'lng'], function(pos) { + return lHlp.isDefined(coords[pos]) && lHlp.isNumber(coords[pos]); + }); + + return ret; + }; + + var _getCoords = function(value) { + if (!value || !_validateCoords(value)) { + return; + } + + var p = null; + if (Array.isArray(value) && value.length === 2) { + p = new Point(value[1], value[0]); + } else if (lHlp.isDefined(value.type) && value.type === 'Point') { + p = new Point(value.coordinates[1], value.coordinates[0]); + } else { + return value; + } + + //note angular.merge is avail in angular 1.4.X we might want to fill it here + return angular.extend(value, p);//tap on lat, lng if it doesnt exist + }; + + return { + getLat: _getLat, + getLng: _getLng, + validateCoords: _validateCoords, + getCoords: _getCoords, + }; +}]); + +angular.module('leaflet-directive').service('leafletHelpers', ["$q", "$log", function($q, $log) { + var _errorHeader = '[AngularJS - Leaflet] '; + var _copy = angular.copy; + var _clone = _copy; + /* + For parsing paths to a field in an object + + Example: + var obj = { + bike:{ + 1: 'hi' + 2: 'foo' + } + }; + _getObjectValue(obj,"bike.1") returns 'hi' + this is getPath in ui-gmap + */ + var _getObjectValue = function(object, pathStr) { + var obj; + if (!object || !angular.isObject(object)) + return; + + //if the key is not a sting then we already have the value + if ((pathStr === null) || !angular.isString(pathStr)) { + return pathStr; + } + + obj = object; + pathStr.split('.').forEach(function(value) { + if (obj) { + obj = obj[value]; + } + }); + + return obj; + }; + + /* + Object Array Notation + _getObjectArrayPath("bike.one.two") + returns: + 'bike["one"]["two"]' + */ + var _getObjectArrayPath = function(pathStr) { + return pathStr.split('.').reduce(function(previous, current) { + return previous + '["' + current + '"]'; + }); + }; + + /* Object Dot Notation + _getObjectPath(["bike","one","two"]) + returns: + "bike.one.two" + */ + var _getObjectDotPath = function(arrayOfStrings) { + return arrayOfStrings.reduce(function(previous, current) { + return previous + '.' + current; + }); + }; + + function _obtainEffectiveMapId(d, mapId) { + var id; + var i; + if (!angular.isDefined(mapId)) { + if (Object.keys(d).length === 0) { + id = 'main'; + } else if (Object.keys(d).length >= 1) { + for (i in d) { + if (d.hasOwnProperty(i)) { + id = i; + } + } + } else { + $log.error(_errorHeader + '- You have more than 1 map on the DOM, you must provide the map ID to the leafletData.getXXX call'); + } + } else { + id = mapId; + } + + return id; + } + + function _getUnresolvedDefer(d, mapId) { + var id = _obtainEffectiveMapId(d, mapId); + var defer; + + if (!angular.isDefined(d[id]) || d[id].resolvedDefer === true) { + defer = $q.defer(); + d[id] = { + defer: defer, + resolvedDefer: false, + }; + } else { + defer = d[id].defer; + } + + return defer; + } + + var _isDefined = function(value) { + return angular.isDefined(value) && value !== null; + }; + + var _isUndefined = function(value) { + return !_isDefined(value); + }; + + // BEGIN DIRECT PORT FROM AngularJS code base + + var SPECIAL_CHARS_REGEXP = /([\:\-\_]+(.))/g; + + var MOZ_HACK_REGEXP = /^moz([A-Z])/; + + var PREFIX_REGEXP = /^((?:x|data)[\:\-_])/i; + + /** + Converts snake_case to camelCase. + Also there is special case for Moz prefix starting with upper case letter. + @param name Name to normalize + */ + + var camelCase = function(name) { + return name.replace(SPECIAL_CHARS_REGEXP, function(_, separator, letter, offset) { + if (offset) { + return letter.toUpperCase(); + } else { + return letter; + } + }).replace(MOZ_HACK_REGEXP, 'Moz$1'); + }; + + /** + Converts all accepted directives format into proper directive name. + @param name Name to normalize + */ + + var directiveNormalize = function(name) { + return camelCase(name.replace(PREFIX_REGEXP, '')); + }; + + // END AngularJS port + + return { + camelCase: camelCase, + directiveNormalize: directiveNormalize, + copy:_copy, + clone:_clone, + errorHeader: _errorHeader, + getObjectValue: _getObjectValue, + getObjectArrayPath:_getObjectArrayPath, + getObjectDotPath: _getObjectDotPath, + defaultTo: function(val, _default) { + return _isDefined(val) ? val : _default; + }, + + //mainly for checking attributes of directives lets keep this minimal (on what we accept) + isTruthy: function(val) { + return val === 'true' || val === true; + }, + + //Determine if a reference is {} + isEmpty: function(value) { + return Object.keys(value).length === 0; + }, + + //Determine if a reference is undefined or {} + isUndefinedOrEmpty: function(value) { + return (angular.isUndefined(value) || value === null) || Object.keys(value).length === 0; + }, + + // Determine if a reference is defined + isDefined: _isDefined, + isUndefined:_isUndefined, + isNumber: angular.isNumber, + isString: angular.isString, + isArray: angular.isArray, + isObject: angular.isObject, + isFunction: angular.isFunction, + equals: angular.equals, + + isValidCenter: function(center) { + return angular.isDefined(center) && angular.isNumber(center.lat) && + angular.isNumber(center.lng) && angular.isNumber(center.zoom); + }, + + isValidPoint: function(point) { + if (!angular.isDefined(point)) { + return false; + } + + if (angular.isArray(point)) { + return point.length === 2 && angular.isNumber(point[0]) && angular.isNumber(point[1]); + } + + return angular.isNumber(point.lat) && angular.isNumber(point.lng); + }, + + isSameCenterOnMap: function(centerModel, map) { + var mapCenter = map.getCenter(); + var zoom = map.getZoom(); + if (centerModel.lat && centerModel.lng && + mapCenter.lat.toFixed(4) === centerModel.lat.toFixed(4) && + mapCenter.lng.toFixed(4) === centerModel.lng.toFixed(4) && + zoom === centerModel.zoom) { + return true; + } + + return false; + }, + + safeApply: function($scope, fn) { + var phase = $scope.$root.$$phase; + if (phase === '$apply' || phase === '$digest') { + $scope.$eval(fn); + } else { + $scope.$evalAsync(fn); + } + }, + + obtainEffectiveMapId: _obtainEffectiveMapId, + + getDefer: function(d, mapId) { + var id = _obtainEffectiveMapId(d, mapId); + var defer; + if (!angular.isDefined(d[id]) || d[id].resolvedDefer === false) { + defer = _getUnresolvedDefer(d, mapId); + } else { + defer = d[id].defer; + } + + return defer; + }, + + getUnresolvedDefer: _getUnresolvedDefer, + + setResolvedDefer: function(d, mapId) { + var id = _obtainEffectiveMapId(d, mapId); + d[id].resolvedDefer = true; + }, + + rangeIsSupported: function() { + var testrange = document.createElement('input'); + testrange.setAttribute('type', 'range'); + return testrange.type === 'range'; + }, + + FullScreenControlPlugin: { + isLoaded: function() { + return angular.isDefined(L.Control.Fullscreen); + }, + }, + + MiniMapControlPlugin: { + isLoaded: function() { + return angular.isDefined(L.Control.MiniMap); + }, + }, + + AwesomeMarkersPlugin: { + isLoaded: function() { + return angular.isDefined(L.AwesomeMarkers) && angular.isDefined(L.AwesomeMarkers.Icon); + }, + + is: function(icon) { + if (this.isLoaded()) { + return icon instanceof L.AwesomeMarkers.Icon; + } else { + return false; + } + }, + + equal: function(iconA, iconB) { + if (!this.isLoaded()) { + return false; + } + + if (this.is(iconA)) { + return angular.equals(iconA, iconB); + } else { + return false; + } + }, + }, + + VectorMarkersPlugin: { + isLoaded: function() { + return angular.isDefined(L.VectorMarkers) && angular.isDefined(L.VectorMarkers.Icon); + }, + + is: function(icon) { + if (this.isLoaded()) { + return icon instanceof L.VectorMarkers.Icon; + } else { + return false; + } + }, + + equal: function(iconA, iconB) { + if (!this.isLoaded()) { + return false; + } + + if (this.is(iconA)) { + return angular.equals(iconA, iconB); + } else { + return false; + } + }, + }, + + DomMarkersPlugin: { + isLoaded: function() { + if (angular.isDefined(L.DomMarkers) && angular.isDefined(L.DomMarkers.Icon)) { + return true; + } else { + return false; + } + }, + + is: function(icon) { + if (this.isLoaded()) { + return icon instanceof L.DomMarkers.Icon; + } else { + return false; + } + }, + + equal: function(iconA, iconB) { + if (!this.isLoaded()) { + return false; + } + + if (this.is(iconA)) { + return angular.equals(iconA, iconB); + } else { + return false; + } + }, + }, + + PolylineDecoratorPlugin: { + isLoaded: function() { + if (angular.isDefined(L.PolylineDecorator)) { + return true; + } else { + return false; + } + }, + + is: function(decoration) { + if (this.isLoaded()) { + return decoration instanceof L.PolylineDecorator; + } else { + return false; + } + }, + + equal: function(decorationA, decorationB) { + if (!this.isLoaded()) { + return false; + } + + if (this.is(decorationA)) { + return angular.equals(decorationA, decorationB); + } else { + return false; + } + }, + }, + + MakiMarkersPlugin: { + isLoaded: function() { + if (angular.isDefined(L.MakiMarkers) && angular.isDefined(L.MakiMarkers.Icon)) { + return true; + } else { + return false; + } + }, + + is: function(icon) { + if (this.isLoaded()) { + return icon instanceof L.MakiMarkers.Icon; + } else { + return false; + } + }, + + equal: function(iconA, iconB) { + if (!this.isLoaded()) { + return false; + } + + if (this.is(iconA)) { + return angular.equals(iconA, iconB); + } else { + return false; + } + }, + }, + ExtraMarkersPlugin: { + isLoaded: function() { + if (angular.isDefined(L.ExtraMarkers) && angular.isDefined(L.ExtraMarkers.Icon)) { + return true; + } else { + return false; + } + }, + + is: function(icon) { + if (this.isLoaded()) { + return icon instanceof L.ExtraMarkers.Icon; + } else { + return false; + } + }, + + equal: function(iconA, iconB) { + if (!this.isLoaded()) { + return false; + } + + if (this.is(iconA)) { + return angular.equals(iconA, iconB); + } else { + return false; + } + }, + }, + LabelPlugin: { + isLoaded: function() { + return angular.isDefined(L.Label); + }, + + is: function(layer) { + if (this.isLoaded()) { + return layer instanceof L.MarkerClusterGroup; + } else { + return false; + } + }, + }, + MarkerClusterPlugin: { + isLoaded: function() { + return angular.isDefined(L.MarkerClusterGroup); + }, + + is: function(layer) { + if (this.isLoaded()) { + return layer instanceof L.MarkerClusterGroup; + } else { + return false; + } + }, + }, + GoogleLayerPlugin: { + isLoaded: function() { + return angular.isDefined(L.Google); + }, + + is: function(layer) { + if (this.isLoaded()) { + return layer instanceof L.Google; + } else { + return false; + } + }, + }, + LeafletProviderPlugin: { + isLoaded: function() { + return angular.isDefined(L.TileLayer.Provider); + }, + + is: function(layer) { + if (this.isLoaded()) { + return layer instanceof L.TileLayer.Provider; + } else { + return false; + } + }, + }, + ChinaLayerPlugin: { + isLoaded: function() { + return angular.isDefined(L.tileLayer.chinaProvider); + }, + }, + HeatLayerPlugin: { + isLoaded: function() { + return angular.isDefined(L.heatLayer); + }, + }, + WebGLHeatMapLayerPlugin: { + isLoaded: function() { + return angular.isDefined(L.TileLayer.WebGLHeatMap); + }, + }, + BingLayerPlugin: { + isLoaded: function() { + return angular.isDefined(L.BingLayer); + }, + + is: function(layer) { + if (this.isLoaded()) { + return layer instanceof L.BingLayer; + } else { + return false; + } + }, + }, + WFSLayerPlugin: { + isLoaded: function() { + return L.GeoJSON.WFS !== undefined; + }, + + is: function(layer) { + if (this.isLoaded()) { + return layer instanceof L.GeoJSON.WFS; + } else { + return false; + } + }, + }, + AGSBaseLayerPlugin: { + isLoaded: function() { + return L.esri !== undefined && L.esri.basemapLayer !== undefined; + }, + + is: function(layer) { + if (this.isLoaded()) { + return layer instanceof L.esri.basemapLayer; + } else { + return false; + } + }, + }, + AGSLayerPlugin: { + isLoaded: function() { + return lvector !== undefined && lvector.AGS !== undefined; + }, + + is: function(layer) { + if (this.isLoaded()) { + return layer instanceof lvector.AGS; + } else { + return false; + } + }, + }, + AGSFeatureLayerPlugin: { + isLoaded: function() { + return L.esri !== undefined && L.esri.featureLayer !== undefined; + }, + + is: function(layer) { + if (this.isLoaded()) { + return layer instanceof L.esri.featureLayer; + } else { + return false; + } + }, + }, + AGSTiledMapLayerPlugin: { + isLoaded: function() { + return L.esri !== undefined && L.esri.tiledMapLayer !== undefined; + }, + + is: function(layer) { + if (this.isLoaded()) { + return layer instanceof L.esri.tiledMapLayer; + } else { + return false; + } + }, + }, + AGSDynamicMapLayerPlugin: { + isLoaded: function() { + return L.esri !== undefined && L.esri.dynamicMapLayer !== undefined; + }, + + is: function(layer) { + if (this.isLoaded()) { + return layer instanceof L.esri.dynamicMapLayer; + } else { + return false; + } + }, + }, + AGSImageMapLayerPlugin: { + isLoaded: function() { + return L.esri !== undefined && L.esri.imageMapLayer !== undefined; + }, + + is: function(layer) { + if (this.isLoaded()) { + return layer instanceof L.esri.imageMapLayer; + } else { + return false; + } + }, + }, + AGSClusteredLayerPlugin: { + isLoaded: function() { + return L.esri !== undefined && L.esri.clusteredFeatureLayer !== undefined; + }, + + is: function(layer) { + if (this.isLoaded()) { + return layer instanceof L.esri.clusteredFeatureLayer; + } else { + return false; + } + }, + }, + AGSHeatmapLayerPlugin: { + isLoaded: function() { + return L.esri !== undefined && L.esri.heatmapFeatureLayer !== undefined; + }, + + is: function(layer) { + if (this.isLoaded()) { + return layer instanceof L.esri.heatmapFeatureLayer; + } else { + return false; + } + }, + }, + YandexLayerPlugin: { + isLoaded: function() { + return angular.isDefined(L.Yandex); + }, + + is: function(layer) { + if (this.isLoaded()) { + return layer instanceof L.Yandex; + } else { + return false; + } + }, + }, + GeoJSONPlugin: { + isLoaded: function() { + return angular.isDefined(L.TileLayer.GeoJSON); + }, + + is: function(layer) { + if (this.isLoaded()) { + return layer instanceof L.TileLayer.GeoJSON; + } else { + return false; + } + }, + }, + UTFGridPlugin: { + isLoaded: function() { + return angular.isDefined(L.UtfGrid); + }, + + is: function(layer) { + if (this.isLoaded()) { + return layer instanceof L.UtfGrid; + } else { + $log.error('[AngularJS - Leaflet] No UtfGrid plugin found.'); + return false; + } + }, + }, + CartoDB: { + isLoaded: function() { + return cartodb; + }, + + is: function(/*layer*/) { + return true; + /* + if (this.isLoaded()) { + return layer instanceof L.TileLayer.GeoJSON; + } else { + return false; + }*/ + }, + }, + Leaflet: { + DivIcon: { + is: function(icon) { + return icon instanceof L.DivIcon; + }, + + equal: function(iconA, iconB) { + if (this.is(iconA)) { + return angular.equals(iconA, iconB); + } else { + return false; + } + }, + }, + Icon: { + is: function(icon) { + return icon instanceof L.Icon; + }, + + equal: function(iconA, iconB) { + if (this.is(iconA)) { + return angular.equals(iconA, iconB); + } else { + return false; + } + }, + }, + }, + /* + watchOptions - object to set deep nested watches and turn off watches all together + (rely on control / functional updates) + watchOptions - Object + doWatch:boolean + isDeep:boolean (sets $watch(function,isDeep)) + individual + doWatch:boolean + isDeep:boolean + */ + + //legacy defaults + watchOptions: { + doWatch:true, + isDeep: true, + individual:{ + doWatch:true, + isDeep: true, + }, + }, + }; +}]); + +angular.module('leaflet-directive').service('leafletIterators', ["$log", "leafletHelpers", function($log, leafletHelpers) { + + var lHlp = leafletHelpers; + var errorHeader = leafletHelpers.errorHeader + 'leafletIterators: '; + + //BEGIN COPY from underscore + var _keys = Object.keys; + var _isFunction = lHlp.isFunction; + var _isObject = lHlp.isObject; + + // Helper for collection methods to determine whether a collection + // should be iterated as an array or as an object + // Related: http://people.mozilla.org/~jorendorff/es6-draft.html#sec-tolength + var MAX_ARRAY_INDEX = Math.pow(2, 53) - 1; + + var _isArrayLike = function(collection) { + var length = collection !== null && collection.length; + return lHlp.isNumber(length) && length >= 0 && length <= MAX_ARRAY_INDEX; + }; + + // Keep the identity function around for default iteratees. + var _identity = function(value) { + return value; + }; + + var _property = function(key) { + return function(obj) { + return obj === null ? void 0 : obj[key]; + }; + }; + + // Internal function that returns an efficient (for current engines) version + // of the passed-in callback, to be repeatedly applied in other Underscore + // functions. + var optimizeCb = function(func, context, argCount) { + if (context === void 0) return func; + switch (argCount === null ? 3 : argCount) { + case 1: return function(value) { + return func.call(context, value); + }; + + case 2: return function(value, other) { + return func.call(context, value, other); + }; + + case 3: return function(value, index, collection) { + return func.call(context, value, index, collection); + }; + + case 4: return function(accumulator, value, index, collection) { + return func.call(context, accumulator, value, index, collection); + }; + } + return function() { + return func.apply(context, arguments); + }; + }; + + // An internal function for creating assigner functions. + var createAssigner = function(keysFunc, undefinedOnly) { + return function(obj) { + var length = arguments.length; + if (length < 2 || obj === null) return obj; + for (var index = 1; index < length; index++) { + var source = arguments[index]; + var keys = keysFunc(source); + var l = keys.length; + + for (var i = 0; i < l; i++) { + var key = keys[i]; + if (!undefinedOnly || obj[key] === void 0) obj[key] = source[key]; + } + } + + return obj; + }; + }; + + // Assigns a given object with all the own properties in the passed-in object(s) + // (https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object/assign) + var _extendOwn; + var _assign = null; + _extendOwn = _assign = createAssigner(_keys); + + // Returns whether an object has a given set of `key:value` pairs. + var _isMatch = function(object, attrs) { + var keys = _keys(attrs); + var length = keys.length; + if (object === null) return !length; + var obj = Object(object); + for (var i = 0; i < length; i++) { + var key = keys[i]; + if (attrs[key] !== obj[key] || !(key in obj)) return false; + } + + return true; + }; + + // Returns a predicate for checking whether an object has a given set of + // `key:value` pairs. + var _matcher; + var _matches = null; + _matcher = _matches = function(attrs) { + attrs = _extendOwn({}, attrs); + return function(obj) { + return _isMatch(obj, attrs); + }; + }; + + // A mostly-internal function to generate callbacks that can be applied + // to each element in a collection, returning the desired result — either + // identity, an arbitrary callback, a property matcher, or a property accessor. + var cb = function(value, context, argCount) { + if (value === null) return _identity; + if (_isFunction(value)) return optimizeCb(value, context, argCount); + if (_isObject(value)) return _matcher(value); + return _property(value); + }; + + var _every; + var _all = null; + _every = _all = function(obj, predicate, context) { + predicate = cb(predicate, context); + var keys = !_isArrayLike(obj) && _keys(obj); + var length = (keys || obj).length; + for (var index = 0; index < length; index++) { + var currentKey = keys ? keys[index] : index; + if (!predicate(obj[currentKey], currentKey, obj)) return false; + } + + return true; + }; + + //END COPY fron underscore + + var _hasErrors = function(collection, cb, ignoreCollection, cbName) { + if (!ignoreCollection) { + if (!lHlp.isDefined(collection) || !lHlp.isDefined(cb)) { + return true; + } + } + + if (!lHlp.isFunction(cb)) { + cbName = lHlp.defaultTo(cb, 'cb'); + $log.error(errorHeader + cbName + ' is not a function'); + return true; + } + + return false; + }; + + var _iterate = function(collection, externalCb, internalCb) { + if (_hasErrors(undefined, internalCb, true, 'internalCb')) { + return; + } + + if (!_hasErrors(collection, externalCb)) { + for (var key in collection) { + if (collection.hasOwnProperty(key)) { + internalCb(collection[key], key); + } + } + } + }; + + //see http://jsperf.com/iterators/3 + //utilizing for in is way faster + var _each = function(collection, cb) { + _iterate(collection, cb, function(val, key) { + cb(val, key); + }); + }; + + return { + each:_each, + forEach: _each, + every: _every, + all: _all, + }; +}]); + +angular.module('leaflet-directive') +.factory('leafletLayerHelpers', ["$rootScope", "$log", "$q", "leafletHelpers", "leafletIterators", function($rootScope, $log, $q, leafletHelpers, leafletIterators) { + var Helpers = leafletHelpers; + var isString = leafletHelpers.isString; + var isObject = leafletHelpers.isObject; + var isArray = leafletHelpers.isArray; + var isDefined = leafletHelpers.isDefined; + var errorHeader = leafletHelpers.errorHeader; + var $it = leafletIterators; + + var utfGridCreateLayer = function(params) { + if (!Helpers.UTFGridPlugin.isLoaded()) { + $log.error('[AngularJS - Leaflet] The UTFGrid plugin is not loaded.'); + return; + } + + var utfgrid = new L.UtfGrid(params.url, params.pluginOptions); + + utfgrid.on('mouseover', function(e) { + $rootScope.$broadcast('leafletDirectiveMap.utfgridMouseover', e); + }); + + utfgrid.on('mouseout', function(e) { + $rootScope.$broadcast('leafletDirectiveMap.utfgridMouseout', e); + }); + + utfgrid.on('click', function(e) { + $rootScope.$broadcast('leafletDirectiveMap.utfgridClick', e); + }); + + utfgrid.on('mousemove', function(e) { + $rootScope.$broadcast('leafletDirectiveMap.utfgridMousemove', e); + }); + + return utfgrid; + }; + + var layerTypes = { + xyz: { + mustHaveUrl: true, + createLayer: function(params) { + return L.tileLayer(params.url, params.options); + }, + }, + mapbox: { + mustHaveKey: true, + createLayer: function(params) { + var version = 3; + if (isDefined(params.options.version) && params.options.version === 4) { + version = params.options.version; + } + + var url = version === 3 ? + '//{s}.tiles.mapbox.com/v3/' + params.key + '/{z}/{x}/{y}.png' : + '//api.tiles.mapbox.com/v4/' + params.key + '/{z}/{x}/{y}.png?access_token=' + params.apiKey; + return L.tileLayer(url, params.options); + }, + }, + geoJSON: { + mustHaveUrl: true, + createLayer: function(params) { + if (!Helpers.GeoJSONPlugin.isLoaded()) { + return; + } + + return new L.TileLayer.GeoJSON(params.url, params.pluginOptions, params.options); + }, + }, + geoJSONShape: { + mustHaveUrl: false, + createLayer: function(params) { + return new L.GeoJSON(params.data, + params.options); + }, + }, + geoJSONAwesomeMarker: { + mustHaveUrl: false, + createLayer: function(params) { + return new L.geoJson(params.data, { + pointToLayer: function(feature, latlng) { + return L.marker(latlng, {icon: L.AwesomeMarkers.icon(params.icon)}); + }, + }); + }, + }, + geoJSONVectorMarker: { + mustHaveUrl: false, + createLayer: function(params) { + return new L.geoJson(params.data, { + pointToLayer: function(feature, latlng) { + return L.marker(latlng, {icon: L.VectorMarkers.icon(params.icon)}); + }, + }); + }, + }, + utfGrid: { + mustHaveUrl: true, + createLayer: utfGridCreateLayer, + }, + cartodbTiles: { + mustHaveKey: true, + createLayer: function(params) { + var url = '//' + params.user + '.cartodb.com/api/v1/map/' + params.key + '/{z}/{x}/{y}.png'; + return L.tileLayer(url, params.options); + }, + }, + cartodbUTFGrid: { + mustHaveKey: true, + mustHaveLayer: true, + createLayer: function(params) { + params.url = '//' + params.user + '.cartodb.com/api/v1/map/' + params.key + '/' + params.layer + '/{z}/{x}/{y}.grid.json'; + return utfGridCreateLayer(params); + }, + }, + cartodbInteractive: { + mustHaveKey: true, + mustHaveLayer: true, + createLayer: function(params) { + var tilesURL = '//' + params.user + '.cartodb.com/api/v1/map/' + params.key + '/{z}/{x}/{y}.png'; + var tileLayer = L.tileLayer(tilesURL, params.options); + params.url = '//' + params.user + '.cartodb.com/api/v1/map/' + params.key + '/' + params.layer + '/{z}/{x}/{y}.grid.json'; + var utfLayer = utfGridCreateLayer(params); + return L.layerGroup([tileLayer, utfLayer]); + }, + }, + wms: { + mustHaveUrl: true, + createLayer: function(params) { + return L.tileLayer.wms(params.url, params.options); + }, + }, + wmts: { + mustHaveUrl: true, + createLayer: function(params) { + return L.tileLayer.wmts(params.url, params.options); + }, + }, + wfs: { + mustHaveUrl: true, + mustHaveLayer: true, + createLayer: function(params) { + if (!Helpers.WFSLayerPlugin.isLoaded()) { + return; + } + + var options = angular.copy(params.options); + if (options.crs && typeof options.crs === 'string') { + /*jshint -W061 */ + options.crs = eval(options.crs); + } + + return new L.GeoJSON.WFS(params.url, params.layer, options); + }, + }, + group: { + mustHaveUrl: false, + createLayer: function(params) { + var lyrs = []; + $it.each(params.options.layers, function(l) { + lyrs.push(createLayer(l)); + }); + + params.options.loadedDefer = function() { + var defers = []; + if (isDefined(params.options.layers)) { + for (var i = 0; i < params.options.layers.length; i++) { + var d = params.options.layers[i].layerOptions.loadedDefer; + if (isDefined(d)) { + defers.push(d); + } + } + } + + return defers; + }; + + return L.layerGroup(lyrs); + }, + }, + featureGroup: { + mustHaveUrl: false, + createLayer: function() { + return L.featureGroup(); + }, + }, + google: { + mustHaveUrl: false, + createLayer: function(params) { + var type = params.type || 'SATELLITE'; + if (!Helpers.GoogleLayerPlugin.isLoaded()) { + return; + } + + return new L.Google(type, params.options); + }, + }, + here: { + mustHaveUrl: false, + createLayer: function(params) { + var provider = params.provider || 'HERE.terrainDay'; + if (!Helpers.LeafletProviderPlugin.isLoaded()) { + return; + } + + return new L.TileLayer.Provider(provider, params.options); + }, + }, + china:{ + mustHaveUrl:false, + createLayer:function(params) { + var type = params.type || ''; + if (!Helpers.ChinaLayerPlugin.isLoaded()) { + return; + } + + return L.tileLayer.chinaProvider(type, params.options); + }, + }, + agsBase: { + mustHaveLayer: true, + createLayer: function(params) { + if (!Helpers.AGSBaseLayerPlugin.isLoaded()) { + return; + } + + return L.esri.basemapLayer(params.layer, params.options); + }, + }, + ags: { + mustHaveUrl: true, + createLayer: function(params) { + if (!Helpers.AGSLayerPlugin.isLoaded()) { + return; + } + + var options = angular.copy(params.options); + angular.extend(options, { + url: params.url, + }); + var layer = new lvector.AGS(options); + layer.onAdd = function(map) { + this.setMap(map); + }; + + layer.onRemove = function() { + this.setMap(null); + }; + + return layer; + }, + }, + agsFeature: { + mustHaveUrl: true, + createLayer: function(params) { + if (!Helpers.AGSFeatureLayerPlugin.isLoaded()) { + $log.warn(errorHeader + ' The esri plugin is not loaded.'); + return; + } + + params.options.url = params.url; + + var layer = L.esri.featureLayer(params.options); + var load = function() { + if (isDefined(params.options.loadedDefer)) { + params.options.loadedDefer.resolve(); + } + }; + + layer.on('loading', function() { + params.options.loadedDefer = $q.defer(); + layer.off('load', load); + layer.on('load', load); + }); + + return layer; + }, + }, + agsTiled: { + mustHaveUrl: true, + createLayer: function(params) { + if (!Helpers.AGSTiledMapLayerPlugin.isLoaded()) { + $log.warn(errorHeader + ' The esri plugin is not loaded.'); + return; + } + + params.options.url = params.url; + + return L.esri.tiledMapLayer(params.options); + }, + }, + agsDynamic: { + mustHaveUrl: true, + createLayer: function(params) { + if (!Helpers.AGSDynamicMapLayerPlugin.isLoaded()) { + $log.warn(errorHeader + ' The esri plugin is not loaded.'); + return; + } + + params.options.url = params.url; + + return L.esri.dynamicMapLayer(params.options); + }, + }, + agsImage: { + mustHaveUrl: true, + createLayer: function(params) { + if (!Helpers.AGSImageMapLayerPlugin.isLoaded()) { + $log.warn(errorHeader + ' The esri plugin is not loaded.'); + return; + } + + params.options.url = params.url; + + return L.esri.imageMapLayer(params.options); + }, + }, + agsClustered: { + mustHaveUrl: true, + createLayer: function(params) { + if (!Helpers.AGSClusteredLayerPlugin.isLoaded()) { + $log.warn(errorHeader + ' The esri clustered layer plugin is not loaded.'); + return; + } + + if (!Helpers.MarkerClusterPlugin.isLoaded()) { + $log.warn(errorHeader + ' The markercluster plugin is not loaded.'); + return; + } + + return L.esri.clusteredFeatureLayer(params.url, params.options); + }, + }, + agsHeatmap: { + mustHaveUrl: true, + createLayer: function(params) { + if (!Helpers.AGSHeatmapLayerPlugin.isLoaded()) { + $log.warn(errorHeader + ' The esri heatmap layer plugin is not loaded.'); + return; + } + + if (!Helpers.HeatLayerPlugin.isLoaded()) { + $log.warn(errorHeader + ' The heatlayer plugin is not loaded.'); + return; + } + + return L.esri.heatmapFeatureLayer(params.url, params.options); + }, + }, + markercluster: { + mustHaveUrl: false, + createLayer: function(params) { + if (!Helpers.MarkerClusterPlugin.isLoaded()) { + $log.warn(errorHeader + ' The markercluster plugin is not loaded.'); + return; + } + + return new L.MarkerClusterGroup(params.options); + }, + }, + bing: { + mustHaveUrl: false, + createLayer: function(params) { + if (!Helpers.BingLayerPlugin.isLoaded()) { + return; + } + + return new L.BingLayer(params.key, params.options); + }, + }, + webGLHeatmap: { + mustHaveUrl: false, + mustHaveData: true, + createLayer: function(params) { + if (!Helpers.WebGLHeatMapLayerPlugin.isLoaded()) { + return; + } + + var layer = new L.TileLayer.WebGLHeatMap(params.options); + if (isDefined(params.data)) { + layer.setData(params.data); + } + + return layer; + }, + }, + heat: { + mustHaveUrl: false, + mustHaveData: true, + createLayer: function(params) { + if (!Helpers.HeatLayerPlugin.isLoaded()) { + return; + } + + var layer = new L.heatLayer(); + + if (isArray(params.data)) { + layer.setLatLngs(params.data); + } + + if (isObject(params.options)) { + layer.setOptions(params.options); + } + + return layer; + }, + }, + yandex: { + mustHaveUrl: false, + createLayer: function(params) { + var type = params.type || 'map'; + if (!Helpers.YandexLayerPlugin.isLoaded()) { + return; + } + + return new L.Yandex(type, params.options); + }, + }, + imageOverlay: { + mustHaveUrl: true, + mustHaveBounds: true, + createLayer: function(params) { + return L.imageOverlay(params.url, params.bounds, params.options); + }, + }, + iip: { + mustHaveUrl: true, + createLayer: function(params) { + return L.tileLayer.iip(params.url, params.options); + }, + }, + + // This "custom" type is used to accept every layer that user want to define himself. + // We can wrap these custom layers like heatmap or yandex, but it means a lot of work/code to wrap the world, + // so we let user to define their own layer outside the directive, + // and pass it on "createLayer" result for next processes + custom: { + createLayer: function(params) { + if (params.layer instanceof L.Class) { + return angular.copy(params.layer); + } else { + $log.error('[AngularJS - Leaflet] A custom layer must be a leaflet Class'); + } + }, + }, + cartodb: { + mustHaveUrl: true, + createLayer: function(params) { + return cartodb.createLayer(params.map, params.url); + }, + }, + }; + + function isValidLayerType(layerDefinition) { + // Check if the baselayer has a valid type + if (!isString(layerDefinition.type)) { + $log.error('[AngularJS - Leaflet] A layer must have a valid type defined.'); + return false; + } + + if (Object.keys(layerTypes).indexOf(layerDefinition.type) === -1) { + $log.error('[AngularJS - Leaflet] A layer must have a valid type: ' + Object.keys(layerTypes)); + return false; + } + + // Check if the layer must have an URL + if (layerTypes[layerDefinition.type].mustHaveUrl && !isString(layerDefinition.url)) { + $log.error('[AngularJS - Leaflet] A base layer must have an url'); + return false; + } + + if (layerTypes[layerDefinition.type].mustHaveData && !isDefined(layerDefinition.data)) { + $log.error('[AngularJS - Leaflet] The base layer must have a "data" array attribute'); + return false; + } + + if (layerTypes[layerDefinition.type].mustHaveLayer && !isDefined(layerDefinition.layer)) { + $log.error('[AngularJS - Leaflet] The type of layer ' + layerDefinition.type + ' must have an layer defined'); + return false; + } + + if (layerTypes[layerDefinition.type].mustHaveBounds && !isDefined(layerDefinition.bounds)) { + $log.error('[AngularJS - Leaflet] The type of layer ' + layerDefinition.type + ' must have bounds defined'); + return false; + } + + if (layerTypes[layerDefinition.type].mustHaveKey && !isDefined(layerDefinition.key)) { + $log.error('[AngularJS - Leaflet] The type of layer ' + layerDefinition.type + ' must have key defined'); + return false; + } + + return true; + } + + function createLayer(layerDefinition) { + if (!isValidLayerType(layerDefinition)) { + return; + } + + if (!isString(layerDefinition.name)) { + $log.error('[AngularJS - Leaflet] A base layer must have a name'); + return; + } + + if (!isObject(layerDefinition.layerParams)) { + layerDefinition.layerParams = {}; + } + + if (!isObject(layerDefinition.layerOptions)) { + layerDefinition.layerOptions = {}; + } + + // Mix the layer specific parameters with the general Leaflet options. Although this is an overhead + // the definition of a base layers is more 'clean' if the two types of parameters are differentiated + for (var attrname in layerDefinition.layerParams) { + layerDefinition.layerOptions[attrname] = layerDefinition.layerParams[attrname]; + } + + var params = { + url: layerDefinition.url, + data: layerDefinition.data, + options: layerDefinition.layerOptions, + layer: layerDefinition.layer, + icon: layerDefinition.icon, + type: layerDefinition.layerType, + bounds: layerDefinition.bounds, + key: layerDefinition.key, + apiKey: layerDefinition.apiKey, + pluginOptions: layerDefinition.pluginOptions, + user: layerDefinition.user, + }; + + //TODO Add $watch to the layer properties + return layerTypes[layerDefinition.type].createLayer(params); + } + + function safeAddLayer(map, layer) { + if (layer && typeof layer.addTo === 'function') { + layer.addTo(map); + } else { + map.addLayer(layer); + } + } + + function safeRemoveLayer(map, layer, layerOptions) { + if (isDefined(layerOptions) && isDefined(layerOptions.loadedDefer)) { + if (angular.isFunction(layerOptions.loadedDefer)) { + var defers = layerOptions.loadedDefer(); + $log.debug('Loaded Deferred', defers); + var count = defers.length; + if (count > 0) { + var resolve = function() { + count--; + if (count === 0) { + map.removeLayer(layer); + } + }; + + for (var i = 0; i < defers.length; i++) { + defers[i].promise.then(resolve); + } + } else { + map.removeLayer(layer); + } + } else { + layerOptions.loadedDefer.promise.then(function() { + map.removeLayer(layer); + }); + } + } else { + map.removeLayer(layer); + } + } + + return { + createLayer: createLayer, + safeAddLayer: safeAddLayer, + safeRemoveLayer: safeRemoveLayer, + }; +}]); + +angular.module('leaflet-directive').factory('leafletLegendHelpers', function() { + var _updateLegend = function(div, legendData, type, url) { + div.innerHTML = ''; + if (legendData.error) { + div.innerHTML += '
        ' + legendData.error.message + '
        '; + } else { + if (type === 'arcgis') { + for (var i = 0; i < legendData.layers.length; i++) { + var layer = legendData.layers[i]; + div.innerHTML += '
        ' + layer.layerName + '
        '; + for (var j = 0; j < layer.legend.length; j++) { + var leg = layer.legend[j]; + div.innerHTML += + '
        ' + + '
        ' + leg.label + '
        '; + } + } + } else if (type === 'image') { + div.innerHTML = ''; + } + } + }; + + var _getOnAddLegend = function(legendData, legendClass, type, url) { + return function(/*map*/) { + var div = L.DomUtil.create('div', legendClass); + + if (!L.Browser.touch) { + L.DomEvent.disableClickPropagation(div); + L.DomEvent.on(div, 'mousewheel', L.DomEvent.stopPropagation); + } else { + L.DomEvent.on(div, 'click', L.DomEvent.stopPropagation); + } + + _updateLegend(div, legendData, type, url); + return div; + }; + }; + + var _getOnAddArrayLegend = function(legend, legendClass) { + return function(/*map*/) { + var div = L.DomUtil.create('div', legendClass); + for (var i = 0; i < legend.colors.length; i++) { + div.innerHTML += + '
        ' + + '
        ' + legend.labels[i] + '
        '; + } + + if (!L.Browser.touch) { + L.DomEvent.disableClickPropagation(div); + L.DomEvent.on(div, 'mousewheel', L.DomEvent.stopPropagation); + } else { + L.DomEvent.on(div, 'click', L.DomEvent.stopPropagation); + } + + return div; + }; + }; + + return { + getOnAddLegend: _getOnAddLegend, + getOnAddArrayLegend: _getOnAddArrayLegend, + updateLegend: _updateLegend, + }; +}); + +angular.module('leaflet-directive').factory('leafletMapDefaults', ["$q", "leafletHelpers", function($q, leafletHelpers) { + function _getDefaults() { + return { + keyboard: true, + dragging: true, + worldCopyJump: false, + doubleClickZoom: true, + scrollWheelZoom: true, + tap: true, + touchZoom: true, + zoomControl: true, + zoomsliderControl: false, + zoomControlPosition: 'topleft', + attributionControl: true, + controls: { + layers: { + visible: true, + position: 'topright', + collapsed: true, + }, + }, + nominatim: { + server: ' http://nominatim.openstreetmap.org/search', + }, + crs: L.CRS.EPSG3857, + tileLayer: '//{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', + tileLayerOptions: { + attribution: '© OpenStreetMap contributors', + }, + path: { + weight: 10, + opacity: 1, + color: '#0000ff', + }, + center: { + lat: 0, + lng: 0, + zoom: 1, + }, + }; + } + + var isDefined = leafletHelpers.isDefined; + var isObject = leafletHelpers.isObject; + var obtainEffectiveMapId = leafletHelpers.obtainEffectiveMapId; + var defaults = {}; + + // Get the _defaults dictionary, and override the properties defined by the user + return { + reset: function() { + defaults = {}; + }, + + getDefaults: function(scopeId) { + var mapId = obtainEffectiveMapId(defaults, scopeId); + return defaults[mapId]; + }, + + getMapCreationDefaults: function(scopeId) { + var mapId = obtainEffectiveMapId(defaults, scopeId); + var d = defaults[mapId]; + + var mapDefaults = { + maxZoom: d.maxZoom, + keyboard: d.keyboard, + dragging: d.dragging, + zoomControl: d.zoomControl, + doubleClickZoom: d.doubleClickZoom, + scrollWheelZoom: d.scrollWheelZoom, + tap: d.tap, + touchZoom: d.touchZoom, + attributionControl: d.attributionControl, + worldCopyJump: d.worldCopyJump, + crs: d.crs, + }; + + if (isDefined(d.minZoom)) { + mapDefaults.minZoom = d.minZoom; + } + + if (isDefined(d.zoomAnimation)) { + mapDefaults.zoomAnimation = d.zoomAnimation; + } + + if (isDefined(d.fadeAnimation)) { + mapDefaults.fadeAnimation = d.fadeAnimation; + } + + if (isDefined(d.markerZoomAnimation)) { + mapDefaults.markerZoomAnimation = d.markerZoomAnimation; + } + + if (d.map) { + for (var option in d.map) { + mapDefaults[option] = d.map[option]; + } + } + + return mapDefaults; + }, + + setDefaults: function(userDefaults, scopeId) { + var newDefaults = _getDefaults(); + + if (isDefined(userDefaults)) { + newDefaults.doubleClickZoom = isDefined(userDefaults.doubleClickZoom) ? userDefaults.doubleClickZoom : newDefaults.doubleClickZoom; + newDefaults.scrollWheelZoom = isDefined(userDefaults.scrollWheelZoom) ? userDefaults.scrollWheelZoom : newDefaults.doubleClickZoom; + newDefaults.tap = isDefined(userDefaults.tap) ? userDefaults.tap : newDefaults.tap; + newDefaults.touchZoom = isDefined(userDefaults.touchZoom) ? userDefaults.touchZoom : newDefaults.doubleClickZoom; + newDefaults.zoomControl = isDefined(userDefaults.zoomControl) ? userDefaults.zoomControl : newDefaults.zoomControl; + newDefaults.zoomsliderControl = isDefined(userDefaults.zoomsliderControl) ? userDefaults.zoomsliderControl : newDefaults.zoomsliderControl; + newDefaults.attributionControl = isDefined(userDefaults.attributionControl) ? userDefaults.attributionControl : newDefaults.attributionControl; + newDefaults.tileLayer = isDefined(userDefaults.tileLayer) ? userDefaults.tileLayer : newDefaults.tileLayer; + newDefaults.zoomControlPosition = isDefined(userDefaults.zoomControlPosition) ? userDefaults.zoomControlPosition : newDefaults.zoomControlPosition; + newDefaults.keyboard = isDefined(userDefaults.keyboard) ? userDefaults.keyboard : newDefaults.keyboard; + newDefaults.dragging = isDefined(userDefaults.dragging) ? userDefaults.dragging : newDefaults.dragging; + + if (isDefined(userDefaults.controls)) { + angular.extend(newDefaults.controls, userDefaults.controls); + } + + if (isObject(userDefaults.crs)) { + newDefaults.crs = userDefaults.crs; + } else if (isDefined(L.CRS[userDefaults.crs])) { + newDefaults.crs = L.CRS[userDefaults.crs]; + } + + if (isDefined(userDefaults.center)) { + angular.copy(userDefaults.center, newDefaults.center); + } + + if (isDefined(userDefaults.tileLayerOptions)) { + angular.copy(userDefaults.tileLayerOptions, newDefaults.tileLayerOptions); + } + + if (isDefined(userDefaults.maxZoom)) { + newDefaults.maxZoom = userDefaults.maxZoom; + } + + if (isDefined(userDefaults.minZoom)) { + newDefaults.minZoom = userDefaults.minZoom; + } + + if (isDefined(userDefaults.zoomAnimation)) { + newDefaults.zoomAnimation = userDefaults.zoomAnimation; + } + + if (isDefined(userDefaults.fadeAnimation)) { + newDefaults.fadeAnimation = userDefaults.fadeAnimation; + } + + if (isDefined(userDefaults.markerZoomAnimation)) { + newDefaults.markerZoomAnimation = userDefaults.markerZoomAnimation; + } + + if (isDefined(userDefaults.worldCopyJump)) { + newDefaults.worldCopyJump = userDefaults.worldCopyJump; + } + + if (isDefined(userDefaults.map)) { + newDefaults.map = userDefaults.map; + } + + if (isDefined(userDefaults.path)) { + newDefaults.path = userDefaults.path; + } + } + + var mapId = obtainEffectiveMapId(defaults, scopeId); + defaults[mapId] = newDefaults; + return newDefaults; + }, + }; +}]); + +angular.module('leaflet-directive').service('leafletMarkersHelpers', ["$rootScope", "$timeout", "leafletHelpers", "$log", "$compile", "leafletGeoJsonHelpers", function($rootScope, $timeout, leafletHelpers, $log, $compile, leafletGeoJsonHelpers) { + var isDefined = leafletHelpers.isDefined; + var defaultTo = leafletHelpers.defaultTo; + var MarkerClusterPlugin = leafletHelpers.MarkerClusterPlugin; + var AwesomeMarkersPlugin = leafletHelpers.AwesomeMarkersPlugin; + var VectorMarkersPlugin = leafletHelpers.VectorMarkersPlugin; + var MakiMarkersPlugin = leafletHelpers.MakiMarkersPlugin; + var ExtraMarkersPlugin = leafletHelpers.ExtraMarkersPlugin; + var DomMarkersPlugin = leafletHelpers.DomMarkersPlugin; + var safeApply = leafletHelpers.safeApply; + var Helpers = leafletHelpers; + var isString = leafletHelpers.isString; + var isNumber = leafletHelpers.isNumber; + var isObject = leafletHelpers.isObject; + var groups = {}; + var geoHlp = leafletGeoJsonHelpers; + var errorHeader = leafletHelpers.errorHeader; + + var _string = function(marker) { + //this exists since JSON.stringify barfs on cyclic + var retStr = ''; + ['_icon', '_latlng', '_leaflet_id', '_map', '_shadow'].forEach(function(prop) { + retStr += prop + ': ' + defaultTo(marker[prop], 'undefined') + ' \n'; + }); + + return '[leafletMarker] : \n' + retStr; + }; + + var _log = function(marker, useConsole) { + var logger = useConsole ? console : $log; + logger.debug(_string(marker)); + }; + + var createLeafletIcon = function(iconData) { + if (isDefined(iconData) && isDefined(iconData.type) && iconData.type === 'awesomeMarker') { + if (!AwesomeMarkersPlugin.isLoaded()) { + $log.error(errorHeader + ' The AwesomeMarkers Plugin is not loaded.'); + } + + return new L.AwesomeMarkers.icon(iconData); + } + + if (isDefined(iconData) && isDefined(iconData.type) && iconData.type === 'vectorMarker') { + if (!VectorMarkersPlugin.isLoaded()) { + $log.error(errorHeader + ' The VectorMarkers Plugin is not loaded.'); + } + + return new L.VectorMarkers.icon(iconData); + } + + if (isDefined(iconData) && isDefined(iconData.type) && iconData.type === 'makiMarker') { + if (!MakiMarkersPlugin.isLoaded()) { + $log.error(errorHeader + 'The MakiMarkers Plugin is not loaded.'); + } + + return new L.MakiMarkers.icon(iconData); + } + + if (isDefined(iconData) && isDefined(iconData.type) && iconData.type === 'extraMarker') { + if (!ExtraMarkersPlugin.isLoaded()) { + $log.error(errorHeader + 'The ExtraMarkers Plugin is not loaded.'); + } + + return new L.ExtraMarkers.icon(iconData); + } + + if (isDefined(iconData) && isDefined(iconData.type) && iconData.type === 'div') { + return new L.divIcon(iconData); + } + + if (isDefined(iconData) && isDefined(iconData.type) && iconData.type === 'dom') { + if (!DomMarkersPlugin.isLoaded()) { + $log.error(errorHeader + 'The DomMarkers Plugin is not loaded.'); + } + + var markerScope = angular.isFunction(iconData.getMarkerScope) ? iconData.getMarkerScope() : $rootScope; + var template = $compile(iconData.template)(markerScope); + var iconDataCopy = angular.copy(iconData); + iconDataCopy.element = template[0]; + return new L.DomMarkers.icon(iconDataCopy); + } + + // allow for any custom icon to be used... assumes the icon has already been initialized + if (isDefined(iconData) && isDefined(iconData.type) && iconData.type === 'icon') { + return iconData.icon; + } + + var base64icon = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABkAAAApCAYAAADAk4LOAAAGmklEQVRYw7VXeUyTZxjvNnfELFuyIzOabermMZEeQC/OclkO49CpOHXOLJl/CAURuYbQi3KLgEhbrhZ1aDwmaoGqKII6odATmH/scDFbdC7LvFqOCc+e95s2VG50X/LLm/f4/Z7neY/ne18aANCmAr5E/xZf1uDOkTcGcWR6hl9247tT5U7Y6SNvWsKT63P58qbfeLJG8M5qcgTknrvvrdDbsT7Ml+tv82X6vVxJE33aRmgSyYtcWVMqX97Yv2JvW39UhRE2HuyBL+t+gK1116ly06EeWFNlAmHxlQE0OMiV6mQCScusKRlhS3QLeVJdl1+23h5dY4FNB3thrbYboqptEFlphTC1hSpJnbRvxP4NWgsE5Jyz86QNNi/5qSUTGuFk1gu54tN9wuK2wc3o+Wc13RCmsoBwEqzGcZsxsvCSy/9wJKf7UWf1mEY8JWfewc67UUoDbDjQC+FqK4QqLVMGGR9d2wurKzqBk3nqIT/9zLxRRjgZ9bqQgub+DdoeCC03Q8j+0QhFhBHR/eP3U/zCln7Uu+hihJ1+bBNffLIvmkyP0gpBZWYXhKussK6mBz5HT6M1Nqpcp+mBCPXosYQfrekGvrjewd59/GvKCE7TbK/04/ZV5QZYVWmDwH1mF3xa2Q3ra3DBC5vBT1oP7PTj4C0+CcL8c7C2CtejqhuCnuIQHaKHzvcRfZpnylFfXsYJx3pNLwhKzRAwAhEqG0SpusBHfAKkxw3w4627MPhoCH798z7s0ZnBJ/MEJbZSbXPhER2ih7p2ok/zSj2cEJDd4CAe+5WYnBCgR2uruyEw6zRoW6/DWJ/OeAP8pd/BGtzOZKpG8oke0SX6GMmRk6GFlyAc59K32OTEinILRJRchah8HQwND8N435Z9Z0FY1EqtxUg+0SO6RJ/mmXz4VuS+DpxXC3gXmZwIL7dBSH4zKE50wESf8qwVgrP1EIlTO5JP9Igu0aexdh28F1lmAEGJGfh7jE6ElyM5Rw/FDcYJjWhbeiBYoYNIpc2FT/SILivp0F1ipDWk4BIEo2VuodEJUifhbiltnNBIXPUFCMpthtAyqws/BPlEF/VbaIxErdxPphsU7rcCp8DohC+GvBIPJS/tW2jtvTmmAeuNO8BNOYQeG8G/2OzCJ3q+soYB5i6NhMaKr17FSal7GIHheuV3uSCY8qYVuEm1cOzqdWr7ku/R0BDoTT+DT+ohCM6/CCvKLKO4RI+dXPeAuaMqksaKrZ7L3FE5FIFbkIceeOZ2OcHO6wIhTkNo0ffgjRGxEqogXHYUPHfWAC/lADpwGcLRY3aeK4/oRGCKYcZXPVoeX/kelVYY8dUGf8V5EBRbgJXT5QIPhP9ePJi428JKOiEYhYXFBqou2Guh+p/mEB1/RfMw6rY7cxcjTrneI1FrDyuzUSRm9miwEJx8E/gUmqlyvHGkneiwErR21F3tNOK5Tf0yXaT+O7DgCvALTUBXdM4YhC/IawPU+2PduqMvuaR6eoxSwUk75ggqsYJ7VicsnwGIkZBSXKOUww73WGXyqP+J2/b9c+gi1YAg/xpwck3gJuucNrh5JvDPvQr0WFXf0piyt8f8/WI0hV4pRxxkQZdJDfDJNOAmM0Ag8jyT6hz0WGXWuP94Yh2jcfjmXAGvHCMslRimDHYuHuDsy2QtHuIavznhbYURq5R57KpzBBRZKPJi8eQg48h4j8SDdowifdIrEVdU+gbO6QNvRRt4ZBthUaZhUnjlYObNagV3keoeru3rU7rcuceqU1mJBxy+BWZYlNEBH+0eH4vRiB+OYybU2hnblYlTvkHinM4m54YnxSyaZYSF6R3jwgP7udKLGIX6r/lbNa9N6y5MFynjWDtrHd75ZvTYAPO/6RgF0k76mQla3FGq7dO+cH8sKn0Vo7nDllwAhqwLPkxrHwWmHJOo+AKJ4rab5OgrM7rVu8eWb2Pu0Dh4eDgXoOfvp7Y7QeqknRmvcTBEyq9m/HQQSCSz6LHq3z0yzsNySRfMS253wl2KyRDbcZPcfJKjZmSEOjcxyi+Y8dUOtsIEH6R2wNykdqrkYJ0RV92H0W58pkfQk7cKevsLK10Py8SdMGfXNXATY+pPbyJR/ET6n9nIfztNtZYRV9XniQu9IA2vOVgy4ir7GCLVmmd+zjkH0eAF9Po6K61pmCXHxU5rHMYd1ftc3owjwRSVRzLjKvqZEty6cRUD7jGqiOdu5HG6MdHjNcNYGqfDm5YRzLBBCCDl/2bk8a8gdbqcfwECu62Fg/HrggAAAABJRU5ErkJggg=='; + var base64shadow = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACkAAAApCAYAAACoYAD2AAAC5ElEQVRYw+2YW4/TMBCF45S0S1luXZCABy5CgLQgwf//S4BYBLTdJLax0fFqmB07nnQfEGqkIydpVH85M+NLjPe++dcPc4Q8Qh4hj5D/AaQJx6H/4TMwB0PeBNwU7EGQAmAtsNfAzoZkgIa0ZgLMa4Aj6CxIAsjhjOCoL5z7Glg1JAOkaicgvQBXuncwJAWjksLtBTWZe04CnYRktUGdilALppZBOgHGZcBzL6OClABvMSVIzyBjazOgrvACf1ydC5mguqAVg6RhdkSWQFj2uxfaq/BrIZOLEWgZdALIDvcMcZLD8ZbLC9de4yR1sYMi4G20S4Q/PWeJYxTOZn5zJXANZHIxAd4JWhPIloTJZhzMQduM89WQ3MUVAE/RnhAXpTycqys3NZALOBbB7kFrgLesQl2h45Fcj8L1tTSohUwuxhy8H/Qg6K7gIs+3kkaigQCOcyEXCHN07wyQazhrmIulvKMQAwMcmLNqyCVyMAI+BuxSMeTk3OPikLY2J1uE+VHQk6ANrhds+tNARqBeaGc72cK550FP4WhXmFmcMGhTwAR1ifOe3EvPqIegFmF+C8gVy0OfAaWQPMR7gF1OQKqGoBjq90HPMP01BUjPOqGFksC4emE48tWQAH0YmvOgF3DST6xieJgHAWxPAHMuNhrImIdvoNOKNWIOcE+UXE0pYAnkX6uhWsgVXDxHdTfCmrEEmMB2zMFimLVOtiiajxiGWrbU52EeCdyOwPEQD8LqyPH9Ti2kgYMf4OhSKB7qYILbBv3CuVTJ11Y80oaseiMWOONc/Y7kJYe0xL2f0BaiFTxknHO5HaMGMublKwxFGzYdWsBF174H/QDknhTHmHHN39iWFnkZx8lPyM8WHfYELmlLKtgWNmFNzQcC1b47gJ4hL19i7o65dhH0Negbca8vONZoP7doIeOC9zXm8RjuL0Gf4d4OYaU5ljo3GYiqzrWQHfJxA6ALhDpVKv9qYeZA8eM3EhfPSCmpuD0AAAAASUVORK5CYII='; + + if (!isDefined(iconData) || !isDefined(iconData.iconUrl)) { + return new L.Icon.Default({ + iconUrl: base64icon, + shadowUrl: base64shadow, + iconSize: [25, 41], + iconAnchor: [12, 41], + popupAnchor: [1, -34], + shadowSize: [41, 41], + }); + } + + return new L.Icon(iconData); + }; + + var _resetMarkerGroup = function(groupName) { + if (isDefined(groups[groupName])) { + groups.splice(groupName, 1); + } + }; + + var _resetMarkerGroups = function() { + groups = {}; + }; + + var _deleteMarker = function(marker, map, layers) { + marker.closePopup(); + + // There is no easy way to know if a marker is added to a layer, so we search for it + // if there are overlays + if (isDefined(layers) && isDefined(layers.overlays)) { + for (var key in layers.overlays) { + if (layers.overlays[key] instanceof L.LayerGroup || layers.overlays[key] instanceof L.FeatureGroup) { + if (layers.overlays[key].hasLayer(marker)) { + layers.overlays[key].removeLayer(marker); + return; + } + } + } + } + + if (isDefined(groups)) { + for (var groupKey in groups) { + if (groups[groupKey].hasLayer(marker)) { + groups[groupKey].removeLayer(marker); + } + } + } + + if (map.hasLayer(marker)) { + map.removeLayer(marker); + } + }; + + var adjustPopupPan = function(marker, map) { + var containerHeight = marker._popup._container.offsetHeight; + var layerPos = new L.Point(marker._popup._containerLeft, -containerHeight - marker._popup._containerBottom); + var containerPos = map.layerPointToContainerPoint(layerPos); + if (containerPos !== null) { + marker._popup._adjustPan(); + } + }; + + var compilePopup = function(marker, markerScope) { + $compile(marker._popup._contentNode)(markerScope); + }; + + var updatePopup = function(marker, markerScope, map) { + //The innerText should be more than 1 once angular has compiled. + //We need to keep trying until angular has compiled before we _updateLayout and _updatePosition + //This should take care of any scenario , eg ngincludes, whatever. + //Is there a better way to check for this? + var innerText = marker._popup._contentNode.innerText || marker._popup._contentNode.textContent; + if (innerText.length < 1) { + $timeout(function() { + updatePopup(marker, markerScope, map); + }); + } + + //cause a reflow - this is also very important - if we don't do this then the widths are from before $compile + var reflow = marker._popup._contentNode.offsetWidth; + + marker._popup._updateLayout(); + marker._popup._updatePosition(); + + if (marker._popup.options.autoPan) { + adjustPopupPan(marker, map); + } + + //using / returning reflow so jshint doesn't moan + return reflow; + }; + + var _manageOpenPopup = function(marker, markerData, map) { + // The marker may provide a scope returning function used to compile the message + // default to $rootScope otherwise + var markerScope = angular.isFunction(markerData.getMessageScope) ? markerData.getMessageScope() : $rootScope; + var compileMessage = isDefined(markerData.compileMessage) ? markerData.compileMessage : true; + + if (compileMessage) { + if (!isDefined(marker._popup) || !isDefined(marker._popup._contentNode)) { + $log.error(errorHeader + 'Popup is invalid or does not have any content.'); + return false; + } + + compilePopup(marker, markerScope); + updatePopup(marker, markerData, map); + } + }; + + var _manageOpenLabel = function(marker, markerData) { + var markerScope = angular.isFunction(markerData.getMessageScope) ? markerData.getMessageScope() : $rootScope; + var labelScope = angular.isFunction(markerData.getLabelScope) ? markerData.getLabelScope() : markerScope; + var compileMessage = isDefined(markerData.compileMessage) ? markerData.compileMessage : true; + + if (Helpers.LabelPlugin.isLoaded() && isDefined(markerData.label)) { + if (isDefined(markerData.label.options) && markerData.label.options.noHide === true) { + marker.showLabel(); + } + + if (compileMessage && isDefined(marker.label)) { + $compile(marker.label._container)(labelScope); + } + } + }; + + var _updateMarker = function(markerData, oldMarkerData, marker, name, leafletScope, layers, map) { + if (!isDefined(oldMarkerData)) { + return; + } + + // Update the lat-lng property (always present in marker properties) + if (!geoHlp.validateCoords(markerData)) { + $log.warn('There are problems with lat-lng data, please verify your marker model'); + _deleteMarker(marker, map, layers); + return; + } + + // watch is being initialized if old and new object is the same + var isInitializing = markerData === oldMarkerData; + + // Update marker rotation + if (isDefined(markerData.iconAngle) && oldMarkerData.iconAngle !== markerData.iconAngle) { + marker.setIconAngle(markerData.iconAngle); + } + + // It is possible that the layer has been removed or the layer marker does not exist + // Update the layer group if present or move it to the map if not + if (!isString(markerData.layer)) { + // There is no layer information, we move the marker to the map if it was in a layer group + if (isString(oldMarkerData.layer)) { + // Remove from the layer group that is supposed to be + if (isDefined(layers.overlays[oldMarkerData.layer]) && layers.overlays[oldMarkerData.layer].hasLayer(marker)) { + layers.overlays[oldMarkerData.layer].removeLayer(marker); + marker.closePopup(); + } + + // Test if it is not on the map and add it + if (!map.hasLayer(marker)) { + map.addLayer(marker); + } + } + } + + if ((isNumber(markerData.opacity) || isNumber(parseFloat(markerData.opacity))) && markerData.opacity !== oldMarkerData.opacity) { + // There was a different opacity so we update it + marker.setOpacity(markerData.opacity); + } + + if (isString(markerData.layer) && oldMarkerData.layer !== markerData.layer) { + // If it was on a layer group we have to remove it + if (isString(oldMarkerData.layer) && isDefined(layers.overlays[oldMarkerData.layer]) && layers.overlays[oldMarkerData.layer].hasLayer(marker)) { + layers.overlays[oldMarkerData.layer].removeLayer(marker); + } + + marker.closePopup(); + + // Remove it from the map in case the new layer is hidden or there is an error in the new layer + if (map.hasLayer(marker)) { + map.removeLayer(marker); + } + + // The markerData.layer is defined so we add the marker to the layer if it is different from the old data + if (!isDefined(layers.overlays[markerData.layer])) { + $log.error(errorHeader + 'You must use a name of an existing layer'); + return; + } + + // Is a group layer? + var layerGroup = layers.overlays[markerData.layer]; + if (!(layerGroup instanceof L.LayerGroup || layerGroup instanceof L.FeatureGroup)) { + $log.error(errorHeader + 'A marker can only be added to a layer of type "group" or "featureGroup"'); + return; + } + + // The marker goes to a correct layer group, so first of all we add it + layerGroup.addLayer(marker); + + // The marker is automatically added to the map depending on the visibility + // of the layer, so we only have to open the popup if the marker is in the map + if (map.hasLayer(marker) && markerData.focus === true) { + marker.openPopup(); + } + } + + // Update the draggable property + if (markerData.draggable !== true && oldMarkerData.draggable === true && (isDefined(marker.dragging))) { + marker.dragging.disable(); + } + + if (markerData.draggable === true && oldMarkerData.draggable !== true) { + // The markerData.draggable property must be true so we update if there wasn't a previous value or it wasn't true + if (marker.dragging) { + marker.dragging.enable(); + } else { + if (L.Handler.MarkerDrag) { + marker.dragging = new L.Handler.MarkerDrag(marker); + marker.options.draggable = true; + marker.dragging.enable(); + } + } + } + + // Update the icon property + if (!isObject(markerData.icon)) { + // If there is no icon property or it's not an object + if (isObject(oldMarkerData.icon)) { + // If there was an icon before restore to the default + marker.setIcon(createLeafletIcon()); + marker.closePopup(); + marker.unbindPopup(); + if (isString(markerData.message)) { + marker.bindPopup(markerData.message, markerData.popupOptions); + } + } + } + + if (isObject(markerData.icon) && isObject(oldMarkerData.icon) && !angular.equals(markerData.icon, oldMarkerData.icon)) { + var dragG = false; + if (marker.dragging) { + dragG = marker.dragging.enabled(); + } + + marker.setIcon(createLeafletIcon(markerData.icon)); + if (dragG) { + marker.dragging.enable(); + } + + marker.closePopup(); + marker.unbindPopup(); + if (isString(markerData.message)) { + marker.bindPopup(markerData.message, markerData.popupOptions); + + // if marker has been already focused, reopen popup + if (map.hasLayer(marker) && markerData.focus === true) { + marker.openPopup(); + } + } + } + + // Update the Popup message property + if (!isString(markerData.message) && isString(oldMarkerData.message)) { + marker.closePopup(); + marker.unbindPopup(); + } + + // Update the label content or bind a new label if the old one has been removed. + if (Helpers.LabelPlugin.isLoaded()) { + if (isDefined(markerData.label) && isDefined(markerData.label.message)) { + if ('label' in oldMarkerData && 'message' in oldMarkerData.label && !angular.equals(markerData.label.message, oldMarkerData.label.message)) { + marker.updateLabelContent(markerData.label.message); + } else if (!angular.isFunction(marker.getLabel) || angular.isFunction(marker.getLabel) && !isDefined(marker.getLabel())) { + marker.bindLabel(markerData.label.message, markerData.label.options); + _manageOpenLabel(marker, markerData); + } else { + _manageOpenLabel(marker, markerData); + } + } else if (!('label' in markerData && !('message' in markerData.label))) { + if (angular.isFunction(marker.unbindLabel)) { + marker.unbindLabel(); + } + } + } + + // There is some text in the popup, so we must show the text or update existing + if (isString(markerData.message) && !isString(oldMarkerData.message)) { + // There was no message before so we create it + marker.bindPopup(markerData.message, markerData.popupOptions); + } + + if (isString(markerData.message) && isString(oldMarkerData.message) && markerData.message !== oldMarkerData.message) { + // There was a different previous message so we update it + marker.setPopupContent(markerData.message); + } + + // Update the focus property + var updatedFocus = false; + if (markerData.focus !== true && oldMarkerData.focus === true) { + // If there was a focus property and was true we turn it off + marker.closePopup(); + updatedFocus = true; + } + + // The markerData.focus property must be true so we update if there wasn't a previous value or it wasn't true + if (markerData.focus === true && (!isDefined(oldMarkerData.focus) || oldMarkerData.focus === false) || (isInitializing && markerData.focus === true)) { + // Reopen the popup when focus is still true + marker.openPopup(); + updatedFocus = true; + } + + // zIndexOffset adjustment + if (oldMarkerData.zIndexOffset !== markerData.zIndexOffset) { + marker.setZIndexOffset(markerData.zIndexOffset); + } + + var markerLatLng = marker.getLatLng(); + var isCluster = (isString(markerData.layer) && Helpers.MarkerClusterPlugin.is(layers.overlays[markerData.layer])); + + // If the marker is in a cluster it has to be removed and added to the layer when the location is changed + if (isCluster) { + // The focus has changed even by a user click or programatically + if (updatedFocus) { + // We only have to update the location if it was changed programatically, because it was + // changed by a user drag the marker data has already been updated by the internal event + // listened by the directive + if ((markerData.lat !== oldMarkerData.lat) || (markerData.lng !== oldMarkerData.lng)) { + layers.overlays[markerData.layer].removeLayer(marker); + marker.setLatLng([markerData.lat, markerData.lng]); + layers.overlays[markerData.layer].addLayer(marker); + } + } else { + // The marker has possibly moved. It can be moved by a user drag (marker location and data are equal but old + // data is diferent) or programatically (marker location and data are diferent) + if ((markerLatLng.lat !== markerData.lat) || (markerLatLng.lng !== markerData.lng)) { + // The marker was moved by a user drag + layers.overlays[markerData.layer].removeLayer(marker); + marker.setLatLng([markerData.lat, markerData.lng]); + layers.overlays[markerData.layer].addLayer(marker); + } else if ((markerData.lat !== oldMarkerData.lat) || (markerData.lng !== oldMarkerData.lng)) { + // The marker was moved programatically + layers.overlays[markerData.layer].removeLayer(marker); + marker.setLatLng([markerData.lat, markerData.lng]); + layers.overlays[markerData.layer].addLayer(marker); + } else if (isObject(markerData.icon) && isObject(oldMarkerData.icon) && !angular.equals(markerData.icon, oldMarkerData.icon)) { + layers.overlays[markerData.layer].removeLayer(marker); + layers.overlays[markerData.layer].addLayer(marker); + } + } + } else if (markerLatLng.lat !== markerData.lat || markerLatLng.lng !== markerData.lng) { + marker.setLatLng([markerData.lat, markerData.lng]); + } + }; + + return { + resetMarkerGroup: _resetMarkerGroup, + + resetMarkerGroups: _resetMarkerGroups, + + deleteMarker: _deleteMarker, + + manageOpenPopup: _manageOpenPopup, + + manageOpenLabel: _manageOpenLabel, + + createMarker: function(markerData) { + if (!isDefined(markerData) || !geoHlp.validateCoords(markerData)) { + $log.error(errorHeader + 'The marker definition is not valid.'); + return; + } + + var coords = geoHlp.getCoords(markerData); + + if (!isDefined(coords)) { + $log.error(errorHeader + 'Unable to get coordinates from markerData.'); + return; + } + + var markerOptions = { + icon: createLeafletIcon(markerData.icon), + title: isDefined(markerData.title) ? markerData.title : '', + draggable: isDefined(markerData.draggable) ? markerData.draggable : false, + clickable: isDefined(markerData.clickable) ? markerData.clickable : true, + riseOnHover: isDefined(markerData.riseOnHover) ? markerData.riseOnHover : false, + zIndexOffset: isDefined(markerData.zIndexOffset) ? markerData.zIndexOffset : 0, + iconAngle: isDefined(markerData.iconAngle) ? markerData.iconAngle : 0, + }; + + // Add any other options not added above to markerOptions + for (var markerDatum in markerData) { + if (markerData.hasOwnProperty(markerDatum) && !markerOptions.hasOwnProperty(markerDatum)) { + markerOptions[markerDatum] = markerData[markerDatum]; + } + } + + var marker = new L.marker(coords, markerOptions); + + if (!isString(markerData.message)) { + marker.unbindPopup(); + } + + return marker; + }, + + addMarkerToGroup: function(marker, groupName, groupOptions, map) { + if (!isString(groupName)) { + $log.error(errorHeader + 'The marker group you have specified is invalid.'); + return; + } + + if (!MarkerClusterPlugin.isLoaded()) { + $log.error(errorHeader + 'The MarkerCluster plugin is not loaded.'); + return; + } + + if (!isDefined(groups[groupName])) { + groups[groupName] = new L.MarkerClusterGroup(groupOptions); + map.addLayer(groups[groupName]); + } + + groups[groupName].addLayer(marker); + }, + + listenMarkerEvents: function(marker, markerData, leafletScope, doWatch, map) { + marker.on('popupopen', function(/* event */) { + safeApply(leafletScope, function() { + if (isDefined(marker._popup) || isDefined(marker._popup._contentNode)) { + markerData.focus = true; + _manageOpenPopup(marker, markerData, map);//needed since markerData is now a copy + } + }); + }); + + marker.on('popupclose', function(/* event */) { + safeApply(leafletScope, function() { + markerData.focus = false; + }); + }); + + marker.on('add', function(/* event */) { + safeApply(leafletScope, function() { + if ('label' in markerData) + _manageOpenLabel(marker, markerData); + }); + }); + }, + + updateMarker: _updateMarker, + + addMarkerWatcher: function(marker, name, leafletScope, layers, map, isDeepWatch) { + var markerWatchPath = Helpers.getObjectArrayPath('markers.' + name); + isDeepWatch = defaultTo(isDeepWatch, true); + + var clearWatch = leafletScope.$watch(markerWatchPath, function(markerData, oldMarkerData) { + if (!isDefined(markerData)) { + _deleteMarker(marker, map, layers); + clearWatch(); + return; + } + + _updateMarker(markerData, oldMarkerData, marker, name, leafletScope, layers, map); + }, isDeepWatch); + }, + + string: _string, + log: _log, + }; +}]); + +angular.module('leaflet-directive').factory('leafletPathsHelpers', ["$rootScope", "$log", "leafletHelpers", function($rootScope, $log, leafletHelpers) { + var isDefined = leafletHelpers.isDefined; + var isArray = leafletHelpers.isArray; + var isNumber = leafletHelpers.isNumber; + var isValidPoint = leafletHelpers.isValidPoint; + + var availableOptions = [ + + // Path options + 'stroke', 'weight', 'color', 'opacity', + 'fill', 'fillColor', 'fillOpacity', + 'dashArray', 'lineCap', 'lineJoin', 'clickable', + 'pointerEvents', 'className', + + // Polyline options + 'smoothFactor', 'noClip', + ]; + function _convertToLeafletLatLngs(latlngs) { + return latlngs.filter(function(latlng) { + return isValidPoint(latlng); + }).map(function(latlng) { + return _convertToLeafletLatLng(latlng); + }); + } + + function _convertToLeafletLatLng(latlng) { + if (isArray(latlng)) { + return new L.LatLng(latlng[0], latlng[1]); + } else { + return new L.LatLng(latlng.lat, latlng.lng); + } + } + + function _convertToLeafletMultiLatLngs(paths) { + return paths.map(function(latlngs) { + return _convertToLeafletLatLngs(latlngs); + }); + } + + function _getOptions(path, defaults) { + var options = {}; + for (var i = 0; i < availableOptions.length; i++) { + var optionName = availableOptions[i]; + + if (isDefined(path[optionName])) { + options[optionName] = path[optionName]; + } else if (isDefined(defaults.path[optionName])) { + options[optionName] = defaults.path[optionName]; + } + } + + return options; + } + + var _updatePathOptions = function(path, data) { + var updatedStyle = {}; + for (var i = 0; i < availableOptions.length; i++) { + var optionName = availableOptions[i]; + if (isDefined(data[optionName])) { + updatedStyle[optionName] = data[optionName]; + } + } + + path.setStyle(data); + }; + + var _isValidPolyline = function(latlngs) { + if (!isArray(latlngs)) { + return false; + } + + for (var i = 0; i < latlngs.length; i++) { + var point = latlngs[i]; + if (!isValidPoint(point)) { + return false; + } + } + + return true; + }; + + var pathTypes = { + polyline: { + isValid: function(pathData) { + var latlngs = pathData.latlngs; + return _isValidPolyline(latlngs); + }, + + createPath: function(options) { + return new L.Polyline([], options); + }, + + setPath: function(path, data) { + path.setLatLngs(_convertToLeafletLatLngs(data.latlngs)); + _updatePathOptions(path, data); + return; + }, + }, + multiPolyline: { + isValid: function(pathData) { + var latlngs = pathData.latlngs; + if (!isArray(latlngs)) { + return false; + } + + for (var i in latlngs) { + var polyline = latlngs[i]; + if (!_isValidPolyline(polyline)) { + return false; + } + } + + return true; + }, + + createPath: function(options) { + return new L.multiPolyline([[[0, 0], [1, 1]]], options); + }, + + setPath: function(path, data) { + path.setLatLngs(_convertToLeafletMultiLatLngs(data.latlngs)); + _updatePathOptions(path, data); + return; + }, + }, + polygon: { + isValid: function(pathData) { + var latlngs = pathData.latlngs; + return _isValidPolyline(latlngs); + }, + + createPath: function(options) { + return new L.Polygon([], options); + }, + + setPath: function(path, data) { + path.setLatLngs(_convertToLeafletLatLngs(data.latlngs)); + _updatePathOptions(path, data); + return; + }, + }, + multiPolygon: { + isValid: function(pathData) { + var latlngs = pathData.latlngs; + + if (!isArray(latlngs)) { + return false; + } + + for (var i in latlngs) { + var polyline = latlngs[i]; + if (!_isValidPolyline(polyline)) { + return false; + } + } + + return true; + }, + + createPath: function(options) { + return new L.MultiPolygon([[[0, 0], [1, 1], [0, 1]]], options); + }, + + setPath: function(path, data) { + path.setLatLngs(_convertToLeafletMultiLatLngs(data.latlngs)); + _updatePathOptions(path, data); + return; + }, + }, + rectangle: { + isValid: function(pathData) { + var latlngs = pathData.latlngs; + + if (!isArray(latlngs) || latlngs.length !== 2) { + return false; + } + + for (var i in latlngs) { + var point = latlngs[i]; + if (!isValidPoint(point)) { + return false; + } + } + + return true; + }, + + createPath: function(options) { + return new L.Rectangle([[0, 0], [1, 1]], options); + }, + + setPath: function(path, data) { + path.setBounds(new L.LatLngBounds(_convertToLeafletLatLngs(data.latlngs))); + _updatePathOptions(path, data); + }, + }, + circle: { + isValid: function(pathData) { + var point = pathData.latlngs; + return isValidPoint(point) && isNumber(pathData.radius); + }, + + createPath: function(options) { + return new L.Circle([0, 0], 1, options); + }, + + setPath: function(path, data) { + path.setLatLng(_convertToLeafletLatLng(data.latlngs)); + if (isDefined(data.radius)) { + path.setRadius(data.radius); + } + + _updatePathOptions(path, data); + }, + }, + circleMarker: { + isValid: function(pathData) { + var point = pathData.latlngs; + return isValidPoint(point) && isNumber(pathData.radius); + }, + + createPath: function(options) { + return new L.CircleMarker([0, 0], options); + }, + + setPath: function(path, data) { + path.setLatLng(_convertToLeafletLatLng(data.latlngs)); + if (isDefined(data.radius)) { + path.setRadius(data.radius); + } + + _updatePathOptions(path, data); + }, + }, + }; + + var _getPathData = function(path) { + var pathData = {}; + if (path.latlngs) { + pathData.latlngs = path.latlngs; + } + + if (path.radius) { + pathData.radius = path.radius; + } + + return pathData; + }; + + return { + setPathOptions: function(leafletPath, pathType, data) { + if (!isDefined(pathType)) { + pathType = 'polyline'; + } + + pathTypes[pathType].setPath(leafletPath, data); + }, + + createPath: function(name, path, defaults) { + if (!isDefined(path.type)) { + path.type = 'polyline'; + } + + var options = _getOptions(path, defaults); + var pathData = _getPathData(path); + + if (!pathTypes[path.type].isValid(pathData)) { + $log.error('[AngularJS - Leaflet] Invalid data passed to the ' + path.type + ' path'); + return; + } + + return pathTypes[path.type].createPath(options); + }, + }; +}]); + +angular.module('leaflet-directive') +.service('leafletWatchHelpers', function() { + + var _maybe = function(scope, watchFunctionName, thingToWatchStr, watchOptions, initCb) { + //watchOptions.isDeep is/should be ignored in $watchCollection + var unWatch = scope[watchFunctionName](thingToWatchStr, function(newValue, oldValue) { + initCb(newValue, oldValue); + if (!watchOptions.doWatch) + unWatch(); + }, watchOptions.isDeep); + + return unWatch; + }; + + /* + @name: maybeWatch + @description: Utility to watch something once or forever. + @returns unWatch function + @param watchOptions - see markersWatchOptions and or derrivatives. This object is used + to set watching to once and its watch depth. + */ + var _maybeWatch = function(scope, thingToWatchStr, watchOptions, initCb) { + return _maybe(scope, '$watch', thingToWatchStr, watchOptions, initCb); + }; + + /* + @name: _maybeWatchCollection + @description: Utility to watch something once or forever. + @returns unWatch function + @param watchOptions - see markersWatchOptions and or derrivatives. This object is used + to set watching to once and its watch depth. + */ + var _maybeWatchCollection = function(scope, thingToWatchStr, watchOptions, initCb) { + return _maybe(scope, '$watchCollection', thingToWatchStr, watchOptions, initCb); + }; + + return { + maybeWatch: _maybeWatch, + maybeWatchCollection: _maybeWatchCollection, + }; +}); + +angular.module('leaflet-directive').factory('nominatimService', ["$q", "$http", "leafletHelpers", "leafletMapDefaults", function($q, $http, leafletHelpers, leafletMapDefaults) { + var isDefined = leafletHelpers.isDefined; + + return { + query: function(address, mapId) { + var defaults = leafletMapDefaults.getDefaults(mapId); + var url = defaults.nominatim.server; + var df = $q.defer(); + + $http.get(url, { params: { format: 'json', limit: 1, q: address } }).success(function(data) { + if (data.length > 0 && isDefined(data[0].boundingbox)) { + df.resolve(data[0]); + } else { + df.reject('[Nominatim] Invalid address'); + } + }); + + return df.promise; + }, + }; +}]); + +angular.module('leaflet-directive').directive('bounds', ["$log", "$timeout", "$http", "leafletHelpers", "nominatimService", "leafletBoundsHelpers", function($log, $timeout, $http, leafletHelpers, nominatimService, leafletBoundsHelpers) { + + return { + restrict: 'A', + scope: false, + replace: false, + require: ['leaflet'], + + link: function(scope, element, attrs, controller) { + var isDefined = leafletHelpers.isDefined; + var createLeafletBounds = leafletBoundsHelpers.createLeafletBounds; + var leafletScope = controller[0].getLeafletScope(); + var mapController = controller[0]; + var errorHeader = leafletHelpers.errorHeader + ' [Bounds] '; + + var emptyBounds = function(bounds) { + return (bounds._southWest.lat === 0 && bounds._southWest.lng === 0 && + bounds._northEast.lat === 0 && bounds._northEast.lng === 0); + }; + + mapController.getMap().then(function(map) { + leafletScope.$on('boundsChanged', function(event) { + var scope = event.currentScope; + var bounds = map.getBounds(); + + if (emptyBounds(bounds) || scope.settingBoundsFromScope) { + return; + } + + scope.settingBoundsFromLeaflet = true; + var newScopeBounds = { + northEast: { + lat: bounds._northEast.lat, + lng: bounds._northEast.lng, + }, + southWest: { + lat: bounds._southWest.lat, + lng: bounds._southWest.lng, + }, + options: bounds.options, + }; + if (!angular.equals(scope.bounds, newScopeBounds)) { + scope.bounds = newScopeBounds; + } + + $timeout(function() { + scope.settingBoundsFromLeaflet = false; + }); + }); + + var lastNominatimQuery; + leafletScope.$watch('bounds', function(bounds) { + if (scope.settingBoundsFromLeaflet) + return; + if (isDefined(bounds.address) && bounds.address !== lastNominatimQuery) { + scope.settingBoundsFromScope = true; + nominatimService.query(bounds.address, attrs.id).then(function(data) { + var b = data.boundingbox; + var newBounds = [[b[0], b[2]], [b[1], b[3]]]; + map.fitBounds(newBounds); + }, function(errMsg) { + + $log.error(errorHeader + ' ' + errMsg + '.'); + }); + + lastNominatimQuery = bounds.address; + $timeout(function() { + scope.settingBoundsFromScope = false; + }); + + return; + } + + var leafletBounds = createLeafletBounds(bounds); + if (leafletBounds && !map.getBounds().equals(leafletBounds)) { + scope.settingBoundsFromScope = true; + map.fitBounds(leafletBounds, bounds.options); + $timeout(function() { + scope.settingBoundsFromScope = false; + }); + } + }, true); + }); + }, + }; +}]); + +var centerDirectiveTypes = ['center', 'lfCenter']; +var centerDirectives = {}; + +centerDirectiveTypes.forEach(function(directiveName) { + centerDirectives[directiveName] = ['$log', '$q', '$location', '$timeout', 'leafletMapDefaults', 'leafletHelpers', + 'leafletBoundsHelpers', 'leafletMapEvents', + function($log, $q, $location, $timeout, leafletMapDefaults, leafletHelpers, + leafletBoundsHelpers, leafletMapEvents) { + + var isDefined = leafletHelpers.isDefined; + var isNumber = leafletHelpers.isNumber; + var isSameCenterOnMap = leafletHelpers.isSameCenterOnMap; + var safeApply = leafletHelpers.safeApply; + var isValidCenter = leafletHelpers.isValidCenter; + var isValidBounds = leafletBoundsHelpers.isValidBounds; + var isUndefinedOrEmpty = leafletHelpers.isUndefinedOrEmpty; + var errorHeader = leafletHelpers.errorHeader; + + var shouldInitializeMapWithBounds = function(bounds, center) { + return isDefined(bounds) && isValidBounds(bounds) && isUndefinedOrEmpty(center); + }; + + var _leafletCenter; + return { + restrict: 'A', + scope: false, + replace: false, + require: 'leaflet', + controller: function() { + _leafletCenter = $q.defer(); + this.getCenter = function() { + return _leafletCenter.promise; + }; + }, + + link: function(scope, element, attrs, controller) { + var leafletScope = controller.getLeafletScope(); + var centerModel = leafletScope[directiveName]; + + controller.getMap().then(function(map) { + var defaults = leafletMapDefaults.getDefaults(attrs.id); + + if (attrs[directiveName].search('-') !== -1) { + $log.error(errorHeader + ' The "center" variable can\'t use a "-" on its key name: "' + attrs[directiveName] + '".'); + map.setView([defaults.center.lat, defaults.center.lng], defaults.center.zoom); + return; + } else if (shouldInitializeMapWithBounds(leafletScope.bounds, centerModel)) { + map.fitBounds(leafletBoundsHelpers.createLeafletBounds(leafletScope.bounds), leafletScope.bounds.options); + centerModel = map.getCenter(); + safeApply(leafletScope, function(scope) { + angular.extend(scope[directiveName], { + lat: map.getCenter().lat, + lng: map.getCenter().lng, + zoom: map.getZoom(), + autoDiscover: false, + }); + }); + + safeApply(leafletScope, function(scope) { + var mapBounds = map.getBounds(); + scope.bounds = { + northEast: { + lat: mapBounds._northEast.lat, + lng: mapBounds._northEast.lng, + }, + southWest: { + lat: mapBounds._southWest.lat, + lng: mapBounds._southWest.lng, + }, + }; + }); + } else if (!isDefined(centerModel)) { + $log.error(errorHeader + ' The "center" property is not defined in the main scope'); + map.setView([defaults.center.lat, defaults.center.lng], defaults.center.zoom); + return; + } else if (!(isDefined(centerModel.lat) && isDefined(centerModel.lng)) && !isDefined(centerModel.autoDiscover)) { + angular.copy(defaults.center, centerModel); + } + + var urlCenterHash; + var mapReady; + if (attrs.urlHashCenter === 'yes') { + var extractCenterFromUrl = function() { + var search = $location.search(); + var centerParam; + if (isDefined(search.c)) { + var cParam = search.c.split(':'); + if (cParam.length === 3) { + centerParam = { + lat: parseFloat(cParam[0]), + lng: parseFloat(cParam[1]), + zoom: parseInt(cParam[2], 10), + }; + } + } + + return centerParam; + }; + + urlCenterHash = extractCenterFromUrl(); + + leafletScope.$on('$locationChangeSuccess', function(event) { + var scope = event.currentScope; + + //$log.debug("updated location..."); + var urlCenter = extractCenterFromUrl(); + if (isDefined(urlCenter) && !isSameCenterOnMap(urlCenter, map)) { + //$log.debug("updating center model...", urlCenter); + angular.extend(scope[directiveName], { + lat: urlCenter.lat, + lng: urlCenter.lng, + zoom: urlCenter.zoom, + }); + } + }); + } + + leafletScope.$watch(directiveName, function(center) { + if (leafletScope.settingCenterFromLeaflet) + return; + + //$log.debug("updated center model..."); + // The center from the URL has priority + if (isDefined(urlCenterHash)) { + angular.copy(urlCenterHash, center); + urlCenterHash = undefined; + } + + if (!isValidCenter(center) && center.autoDiscover !== true) { + $log.warn(errorHeader + ' invalid \'center\''); + + //map.setView([defaults.center.lat, defaults.center.lng], defaults.center.zoom); + return; + } + + if (center.autoDiscover === true) { + if (!isNumber(center.zoom)) { + map.setView([defaults.center.lat, defaults.center.lng], defaults.center.zoom); + } + + if (isNumber(center.zoom) && center.zoom > defaults.center.zoom) { + map.locate({ + setView: true, + maxZoom: center.zoom, + }); + } else if (isDefined(defaults.maxZoom)) { + map.locate({ + setView: true, + maxZoom: defaults.maxZoom, + }); + } else { + map.locate({ + setView: true, + }); + } + + return; + } + + if (mapReady && isSameCenterOnMap(center, map)) { + //$log.debug("no need to update map again."); + return; + } + + //$log.debug("updating map center...", center); + leafletScope.settingCenterFromScope = true; + map.setView([center.lat, center.lng], center.zoom); + leafletMapEvents.notifyCenterChangedToBounds(leafletScope, map); + $timeout(function() { + leafletScope.settingCenterFromScope = false; + + //$log.debug("allow center scope updates"); + }); + }, true); + + map.whenReady(function() { + mapReady = true; + }); + + map.on('moveend', function(/* event */) { + // Resolve the center after the first map position + _leafletCenter.resolve(); + leafletMapEvents.notifyCenterUrlHashChanged(leafletScope, map, attrs, $location.search()); + + //$log.debug("updated center on map..."); + if (isSameCenterOnMap(centerModel, map) || leafletScope.settingCenterFromScope) { + //$log.debug("same center in model, no need to update again."); + return; + } + + leafletScope.settingCenterFromLeaflet = true; + safeApply(leafletScope, function(scope) { + if (!leafletScope.settingCenterFromScope) { + //$log.debug("updating center model...", map.getCenter(), map.getZoom()); + angular.extend(scope[directiveName], { + lat: map.getCenter().lat, + lng: map.getCenter().lng, + zoom: map.getZoom(), + autoDiscover: false, + }); + } + + leafletMapEvents.notifyCenterChangedToBounds(leafletScope, map); + $timeout(function() { + leafletScope.settingCenterFromLeaflet = false; + }); + }); + }); + + if (centerModel.autoDiscover === true) { + map.on('locationerror', function() { + $log.warn(errorHeader + ' The Geolocation API is unauthorized on this page.'); + if (isValidCenter(centerModel)) { + map.setView([centerModel.lat, centerModel.lng], centerModel.zoom); + leafletMapEvents.notifyCenterChangedToBounds(leafletScope, map); + } else { + map.setView([defaults.center.lat, defaults.center.lng], defaults.center.zoom); + leafletMapEvents.notifyCenterChangedToBounds(leafletScope, map); + } + }); + } + }); + }, + }; + }, + ]; +}); + +centerDirectiveTypes.forEach(function(dirType) { + angular.module('leaflet-directive').directive(dirType, centerDirectives[dirType]); +}); + +angular.module('leaflet-directive').directive('controls', ["$log", "leafletHelpers", "leafletControlHelpers", function($log, leafletHelpers, leafletControlHelpers) { + + return { + restrict: 'A', + scope: false, + replace: false, + require: '?^leaflet', + + link: function(scope, element, attrs, controller) { + if (!controller) { + return; + } + + var createControl = leafletControlHelpers.createControl; + var isValidControlType = leafletControlHelpers.isValidControlType; + var leafletScope = controller.getLeafletScope(); + var isDefined = leafletHelpers.isDefined; + var isArray = leafletHelpers.isArray; + var leafletControls = {}; + var errorHeader = leafletHelpers.errorHeader + ' [Controls] '; + + controller.getMap().then(function(map) { + + leafletScope.$watchCollection('controls', function(newControls) { + + // Delete controls from the array + for (var name in leafletControls) { + if (!isDefined(newControls[name])) { + if (map.hasControl(leafletControls[name])) { + map.removeControl(leafletControls[name]); + } + + delete leafletControls[name]; + } + } + + for (var newName in newControls) { + var control; + + var controlType = isDefined(newControls[newName].type) ? newControls[newName].type : newName; + + if (!isValidControlType(controlType)) { + $log.error(errorHeader + ' Invalid control type: ' + controlType + '.'); + return; + } + + if (controlType !== 'custom') { + control = createControl(controlType, newControls[newName]); + map.addControl(control); + leafletControls[newName] = control; + } else { + var customControlValue = newControls[newName]; + if (isArray(customControlValue)) { + for (var i in customControlValue) { + var customControl = customControlValue[i]; + map.addControl(customControl); + leafletControls[newName] = !isDefined(leafletControls[newName]) ? [customControl] : leafletControls[newName].concat([customControl]); + } + } else { + map.addControl(customControlValue); + leafletControls[newName] = customControlValue; + } + } + } + + }); + + }); + }, + }; +}]); + +angular.module('leaflet-directive').directive('decorations', ["$log", "leafletHelpers", function($log, leafletHelpers) { + + return { + restrict: 'A', + scope: false, + replace: false, + require: 'leaflet', + + link: function(scope, element, attrs, controller) { + var leafletScope = controller.getLeafletScope(); + var PolylineDecoratorPlugin = leafletHelpers.PolylineDecoratorPlugin; + var isDefined = leafletHelpers.isDefined; + var leafletDecorations = {}; + + /* Creates an "empty" decoration with a set of coordinates, but no pattern. */ + function createDecoration(options) { + if (isDefined(options) && isDefined(options.coordinates)) { + if (!PolylineDecoratorPlugin.isLoaded()) { + $log.error('[AngularJS - Leaflet] The PolylineDecorator Plugin is not loaded.'); + } + } + + return L.polylineDecorator(options.coordinates); + } + + /* Updates the path and the patterns for the provided decoration, and returns the decoration. */ + function setDecorationOptions(decoration, options) { + if (isDefined(decoration) && isDefined(options)) { + if (isDefined(options.coordinates) && isDefined(options.patterns)) { + decoration.setPaths(options.coordinates); + decoration.setPatterns(options.patterns); + return decoration; + } + } + } + + controller.getMap().then(function(map) { + leafletScope.$watch('decorations', function(newDecorations) { + for (var name in leafletDecorations) { + if (!isDefined(newDecorations[name]) || !angular.equals(newDecorations[name], leafletDecorations)) { + map.removeLayer(leafletDecorations[name]); + delete leafletDecorations[name]; + } + } + + for (var newName in newDecorations) { + var decorationData = newDecorations[newName]; + var newDecoration = createDecoration(decorationData); + + if (isDefined(newDecoration)) { + leafletDecorations[newName] = newDecoration; + map.addLayer(newDecoration); + setDecorationOptions(newDecoration, decorationData); + } + } + }, true); + }); + }, + }; +}]); + +angular.module('leaflet-directive').directive('eventBroadcast', ["$log", "$rootScope", "leafletHelpers", "leafletMapEvents", "leafletIterators", function($log, $rootScope, leafletHelpers, leafletMapEvents, leafletIterators) { + + return { + restrict: 'A', + scope: false, + replace: false, + require: 'leaflet', + + link: function(scope, element, attrs, controller) { + var isObject = leafletHelpers.isObject; + var isDefined = leafletHelpers.isDefined; + var leafletScope = controller.getLeafletScope(); + var eventBroadcast = leafletScope.eventBroadcast; + var availableMapEvents = leafletMapEvents.getAvailableMapEvents(); + var addEvents = leafletMapEvents.addEvents; + + controller.getMap().then(function(map) { + + var mapEvents = []; + var logic = 'broadcast'; + + // We have a possible valid object + if (!isDefined(eventBroadcast.map)) { + // We do not have events enable/disable do we do nothing (all enabled by default) + mapEvents = availableMapEvents; + } else if (!isObject(eventBroadcast.map)) { + // Not a valid object + $log.warn('[AngularJS - Leaflet] event-broadcast.map must be an object check your model.'); + } else { + // We have a possible valid map object + // Event propadation logic + if (eventBroadcast.map.logic !== 'emit' && eventBroadcast.map.logic !== 'broadcast') { + // This is an error + $log.warn('[AngularJS - Leaflet] Available event propagation logic are: \'emit\' or \'broadcast\'.'); + } else { + logic = eventBroadcast.map.logic; + } + + if (!(isObject(eventBroadcast.map.enable) && eventBroadcast.map.enable.length >= 0)) { + $log.warn('[AngularJS - Leaflet] event-broadcast.map.enable must be an object check your model.'); + } else { + // Enable events + leafletIterators.each(eventBroadcast.map.enable, function(eventName) { + // Do we have already the event enabled? + if (mapEvents.indexOf(eventName) === -1 && availableMapEvents.indexOf(eventName) !== -1) { + mapEvents.push(eventName); + } + }); + } + + } + + // as long as the map is removed in the root leaflet directive we + // do not need ot clean up the events as leaflet does it itself + addEvents(map, mapEvents, 'eventName', leafletScope, logic); + }); + }, + }; +}]); + +angular.module('leaflet-directive') +.directive('geojson', ["$log", "$rootScope", "leafletData", "leafletHelpers", "leafletWatchHelpers", "leafletDirectiveControlsHelpers", "leafletIterators", "leafletGeoJsonEvents", function($log, $rootScope, leafletData, leafletHelpers, + leafletWatchHelpers, leafletDirectiveControlsHelpers, leafletIterators, leafletGeoJsonEvents) { + var _maybeWatch = leafletWatchHelpers.maybeWatch; + var _watchOptions = leafletHelpers.watchOptions; + var _extendDirectiveControls = leafletDirectiveControlsHelpers.extend; + var hlp = leafletHelpers; + var $it = leafletIterators; + + return { + restrict: 'A', + scope: false, + replace: false, + require: 'leaflet', + + link: function(scope, element, attrs, controller) { + var isDefined = leafletHelpers.isDefined; + var leafletScope = controller.getLeafletScope(); + var leafletGeoJSON = {}; + var _hasSetLeafletData = false; + + controller.getMap().then(function(map) { + var watchOptions = leafletScope.geojsonWatchOptions || _watchOptions; + + var _hookUpEvents = function(geojson, maybeName) { + var onEachFeature; + + if (angular.isFunction(geojson.onEachFeature)) { + onEachFeature = geojson.onEachFeature; + } else { + onEachFeature = function(feature, layer) { + if (leafletHelpers.LabelPlugin.isLoaded() && isDefined(feature.properties.description)) { + layer.bindLabel(feature.properties.description); + } + + leafletGeoJsonEvents.bindEvents(attrs.id, layer, null, feature, + leafletScope, maybeName, + {resetStyleOnMouseout: geojson.resetStyleOnMouseout, + mapId: attrs.id, }); + }; + } + + return onEachFeature; + }; + + var isNested = (hlp.isDefined(attrs.geojsonNested) && + hlp.isTruthy(attrs.geojsonNested)); + + var _clean = function() { + if (!leafletGeoJSON) + return; + var _remove = function(lObject) { + if (isDefined(lObject) && map.hasLayer(lObject)) { + map.removeLayer(lObject); + } + }; + + if (isNested) { + $it.each(leafletGeoJSON, function(lObject) { + _remove(lObject); + }); + + return; + } + + _remove(leafletGeoJSON); + }; + + var _addGeojson = function(model, maybeName) { + var geojson = angular.copy(model); + if (!(isDefined(geojson) && isDefined(geojson.data))) { + return; + } + + var onEachFeature = _hookUpEvents(geojson, maybeName); + + if (!isDefined(geojson.options)) { + //right here is why we use a clone / copy (we modify and thus) + //would kick of a watcher.. we need to be more careful everywhere + //for stuff like this + geojson.options = { + style: geojson.style, + filter: geojson.filter, + onEachFeature: onEachFeature, + pointToLayer: geojson.pointToLayer, + }; + } + + var lObject = L.geoJson(geojson.data, geojson.options); + + if (maybeName && hlp.isString(maybeName)) { + leafletGeoJSON[maybeName] = lObject; + } else { + leafletGeoJSON = lObject; + } + + lObject.addTo(map); + + if (!_hasSetLeafletData) {//only do this once and play with the same ref forever + _hasSetLeafletData = true; + leafletData.setGeoJSON(leafletGeoJSON, attrs.id); + } + }; + + var _create = function(model) { + _clean(); + if (isNested) { + if (!model || !Object.keys(model).length) + return; + $it.each(model, function(m, name) { + //name could be layerName and or groupName + //for now it is not tied to a layer + _addGeojson(m, name); + }); + + return; + } + + _addGeojson(model); + }; + + _extendDirectiveControls(attrs.id, 'geojson', _create, _clean); + + _maybeWatch(leafletScope, 'geojson', watchOptions, function(geojson) { + _create(geojson); + }); + }); + }, + }; +}]); + +angular.module('leaflet-directive').directive('layercontrol', ["$filter", "$log", "leafletData", "leafletHelpers", function($filter, $log, leafletData, leafletHelpers) { + + return { + restrict: 'E', + scope: { + icons: '=?', + autoHideOpacity: '=?', // Hide other opacity controls when one is activated. + showGroups: '=?', // Hide other opacity controls when one is activated. + title: '@', + baseTitle: '@', + overlaysTitle: '@', + }, + replace: true, + transclude: false, + require: '^leaflet', + controller: ["$scope", "$element", "$sce", function($scope, $element, $sce) { + $log.debug('[Angular Directive - Layers] layers', $scope, $element); + var safeApply = leafletHelpers.safeApply; + var isDefined = leafletHelpers.isDefined; + angular.extend($scope, { + baselayer: '', + oldGroup: '', + layerProperties: {}, + groupProperties: {}, + rangeIsSupported: leafletHelpers.rangeIsSupported(), + changeBaseLayer: function(key, e) { + leafletHelpers.safeApply($scope, function(scp) { + scp.baselayer = key; + leafletData.getMap().then(function(map) { + leafletData.getLayers().then(function(leafletLayers) { + if (map.hasLayer(leafletLayers.baselayers[key])) { + return; + } + + for (var i in scp.layers.baselayers) { + scp.layers.baselayers[i].icon = scp.icons.unradio; + if (map.hasLayer(leafletLayers.baselayers[i])) { + map.removeLayer(leafletLayers.baselayers[i]); + } + } + + map.addLayer(leafletLayers.baselayers[key]); + scp.layers.baselayers[key].icon = $scope.icons.radio; + }); + }); + }); + + e.preventDefault(); + }, + + moveLayer: function(ly, newIndex, e) { + var delta = Object.keys($scope.layers.baselayers).length; + if (newIndex >= (1 + delta) && newIndex <= ($scope.overlaysArray.length + delta)) { + var oldLy; + for (var key in $scope.layers.overlays) { + if ($scope.layers.overlays[key].index === newIndex) { + oldLy = $scope.layers.overlays[key]; + break; + } + } + + if (oldLy) { + safeApply($scope, function() { + oldLy.index = ly.index; + ly.index = newIndex; + }); + } + } + + e.stopPropagation(); + e.preventDefault(); + }, + + initIndex: function(layer, idx) { + var delta = Object.keys($scope.layers.baselayers).length; + layer.index = isDefined(layer.index) ? layer.index : idx + delta + 1; + }, + + initGroup: function(groupName) { + $scope.groupProperties[groupName] = $scope.groupProperties[groupName] ? $scope.groupProperties[groupName] : {}; + }, + + toggleOpacity: function(e, layer) { + if (layer.visible) { + if ($scope.autoHideOpacity && !$scope.layerProperties[layer.name].opacityControl) { + for (var k in $scope.layerProperties) { + $scope.layerProperties[k].opacityControl = false; + } + } + + $scope.layerProperties[layer.name].opacityControl = !$scope.layerProperties[layer.name].opacityControl; + } + + e.stopPropagation(); + e.preventDefault(); + }, + + toggleLegend: function(layer) { + $scope.layerProperties[layer.name].showLegend = !$scope.layerProperties[layer.name].showLegend; + }, + + showLegend: function(layer) { + return layer.legend && $scope.layerProperties[layer.name].showLegend; + }, + + unsafeHTML: function(html) { + return $sce.trustAsHtml(html); + }, + + getOpacityIcon: function(layer) { + return layer.visible && $scope.layerProperties[layer.name].opacityControl ? $scope.icons.close : $scope.icons.open; + }, + + getGroupIcon: function(group) { + return group.visible ? $scope.icons.check : $scope.icons.uncheck; + }, + + changeOpacity: function(layer) { + var op = $scope.layerProperties[layer.name].opacity; + leafletData.getMap().then(function(map) { + leafletData.getLayers().then(function(leafletLayers) { + var ly; + for (var k in $scope.layers.overlays) { + if ($scope.layers.overlays[k] === layer) { + ly = leafletLayers.overlays[k]; + break; + } + } + + if (map.hasLayer(ly)) { + if (ly.setOpacity) { + ly.setOpacity(op / 100); + } + + if (ly.getLayers && ly.eachLayer) { + ly.eachLayer(function(lay) { + if (lay.setOpacity) { + lay.setOpacity(op / 100); + } + }); + } + } + }); + }); + }, + + changeGroupVisibility: function(groupName) { + if (!isDefined($scope.groupProperties[groupName])) { + return; + } + + var visible = $scope.groupProperties[groupName].visible; + for (var k in $scope.layers.overlays) { + var layer = $scope.layers.overlays[k]; + if (layer.group === groupName) { + layer.visible = visible; + } + } + }, + }); + + var div = $element.get(0); + if (!L.Browser.touch) { + L.DomEvent.disableClickPropagation(div); + L.DomEvent.on(div, 'mousewheel', L.DomEvent.stopPropagation); + } else { + L.DomEvent.on(div, 'click', L.DomEvent.stopPropagation); + } + }], + + template: + '
        ' + + '

        {{ title }}

        ' + + '
        ' + + '
        {{ baseTitle }}
        ' + + '
        ' + + '' + + '
        ' + + '
        ' + + '
        ' + + '
        {{ overlaysTitle }}
        ' + + '
        ' + + '
        ' + + '' + + '' + + '
        ' + + ' ' + + ' ' + + ' ' + + '' + + '
        ' + + '
        ' + + '
        ' + + '' + + '' + + '' + + '
        Range is not supported in this browser
        ' + + '
        ' + + '
        ' + + '
        ' + + '
        ' + + '
        ', + link: function(scope, element, attrs, controller) { + var isDefined = leafletHelpers.isDefined; + var leafletScope = controller.getLeafletScope(); + var layers = leafletScope.layers; + + scope.$watch('icons', function() { + var defaultIcons = { + uncheck: 'fa fa-square-o', + check: 'fa fa-check-square-o', + radio: 'fa fa-dot-circle-o', + unradio: 'fa fa-circle-o', + up: 'fa fa-angle-up', + down: 'fa fa-angle-down', + open: 'fa fa-angle-double-down', + close: 'fa fa-angle-double-up', + toggleLegend: 'fa fa-pencil-square-o', + }; + if (isDefined(scope.icons)) { + angular.extend(defaultIcons, scope.icons); + angular.extend(scope.icons, defaultIcons); + } else { + scope.icons = defaultIcons; + } + }); + + // Setting layer stack order. + attrs.order = (isDefined(attrs.order) && (attrs.order === 'normal' || attrs.order === 'reverse')) ? attrs.order : 'normal'; + scope.order = attrs.order === 'normal'; + scope.orderNumber = attrs.order === 'normal' ? -1 : 1; + + scope.layers = layers; + controller.getMap().then(function(map) { + leafletScope.$watch('layers.baselayers', function(newBaseLayers) { + var baselayersArray = {}; + leafletData.getLayers().then(function(leafletLayers) { + var key; + for (key in newBaseLayers) { + var layer = newBaseLayers[key]; + layer.icon = scope.icons[map.hasLayer(leafletLayers.baselayers[key]) ? 'radio' : 'unradio']; + baselayersArray[key] = layer; + } + + scope.baselayersArray = baselayersArray; + }); + }); + + leafletScope.$watch('layers.overlays', function(newOverlayLayers) { + var overlaysArray = []; + var groupVisibleCount = {}; + leafletData.getLayers().then(function(leafletLayers) { + var key; + for (key in newOverlayLayers) { + var layer = newOverlayLayers[key]; + layer.icon = scope.icons[(layer.visible ? 'check' : 'uncheck')]; + overlaysArray.push(layer); + if (!isDefined(scope.layerProperties[layer.name])) { + scope.layerProperties[layer.name] = { + opacity: isDefined(layer.layerOptions.opacity) ? layer.layerOptions.opacity * 100 : 100, + opacityControl: false, + showLegend: true, + }; + } + + if (isDefined(layer.group)) { + if (!isDefined(scope.groupProperties[layer.group])) { + scope.groupProperties[layer.group] = { + visible: false, + }; + } + + groupVisibleCount[layer.group] = isDefined(groupVisibleCount[layer.group]) ? groupVisibleCount[layer.group] : { + count: 0, + visibles: 0, + }; + groupVisibleCount[layer.group].count++; + if (layer.visible) { + groupVisibleCount[layer.group].visibles++; + } + } + + if (isDefined(layer.index) && leafletLayers.overlays[key].setZIndex) { + leafletLayers.overlays[key].setZIndex(newOverlayLayers[key].index); + } + } + + for (key in groupVisibleCount) { + scope.groupProperties[key].visible = groupVisibleCount[key].visibles === groupVisibleCount[key].count; + } + + scope.overlaysArray = overlaysArray; + }); + }, true); + }); + }, + }; +}]); + +angular.module('leaflet-directive').directive('layers', ["$log", "$q", "leafletData", "leafletHelpers", "leafletLayerHelpers", "leafletControlHelpers", function($log, $q, leafletData, leafletHelpers, leafletLayerHelpers, leafletControlHelpers) { + + return { + restrict: 'A', + scope: false, + replace: false, + require: 'leaflet', + controller: ["$scope", function($scope) { + $scope._leafletLayers = $q.defer(); + this.getLayers = function() { + return $scope._leafletLayers.promise; + }; + }], + + link: function(scope, element, attrs, controller) { + var isDefined = leafletHelpers.isDefined; + var leafletLayers = {}; + var leafletScope = controller.getLeafletScope(); + var layers = leafletScope.layers; + var createLayer = leafletLayerHelpers.createLayer; + var safeAddLayer = leafletLayerHelpers.safeAddLayer; + var safeRemoveLayer = leafletLayerHelpers.safeRemoveLayer; + var updateLayersControl = leafletControlHelpers.updateLayersControl; + var isLayersControlVisible = false; + + controller.getMap().then(function(map) { + + // We have baselayers to add to the map + scope._leafletLayers.resolve(leafletLayers); + leafletData.setLayers(leafletLayers, attrs.id); + + leafletLayers.baselayers = {}; + leafletLayers.overlays = {}; + + var mapId = attrs.id; + + // Setup all baselayers definitions + var oneVisibleLayer = false; + for (var layerName in layers.baselayers) { + var newBaseLayer = createLayer(layers.baselayers[layerName]); + if (!isDefined(newBaseLayer)) { + delete layers.baselayers[layerName]; + continue; + } + + leafletLayers.baselayers[layerName] = newBaseLayer; + + // Only add the visible layer to the map, layer control manages the addition to the map + // of layers in its control + if (layers.baselayers[layerName].top === true) { + safeAddLayer(map, leafletLayers.baselayers[layerName]); + oneVisibleLayer = true; + } + } + + // If there is no visible layer add first to the map + if (!oneVisibleLayer && Object.keys(leafletLayers.baselayers).length > 0) { + safeAddLayer(map, leafletLayers.baselayers[Object.keys(layers.baselayers)[0]]); + } + + // Setup the Overlays + for (layerName in layers.overlays) { + //if (layers.overlays[layerName].type === 'cartodb') { + // + //} + + var newOverlayLayer = createLayer(layers.overlays[layerName]); + if (!isDefined(newOverlayLayer)) { + delete layers.overlays[layerName]; + continue; + } + + leafletLayers.overlays[layerName] = newOverlayLayer; + + // Only add the visible overlays to the map + if (layers.overlays[layerName].visible === true) { + safeAddLayer(map, leafletLayers.overlays[layerName]); + } + } + + // Watch for the base layers + leafletScope.$watch('layers.baselayers', function(newBaseLayers, oldBaseLayers) { + if (angular.equals(newBaseLayers, oldBaseLayers)) { + isLayersControlVisible = updateLayersControl(map, mapId, isLayersControlVisible, newBaseLayers, layers.overlays, leafletLayers); + return true; + } + + // Delete layers from the array + for (var name in leafletLayers.baselayers) { + if (!isDefined(newBaseLayers[name]) || newBaseLayers[name].doRefresh) { + // Remove from the map if it's on it + if (map.hasLayer(leafletLayers.baselayers[name])) { + map.removeLayer(leafletLayers.baselayers[name]); + } + + delete leafletLayers.baselayers[name]; + + if (newBaseLayers[name] && newBaseLayers[name].doRefresh) { + newBaseLayers[name].doRefresh = false; + } + } + } + + // add new layers + for (var newName in newBaseLayers) { + if (!isDefined(leafletLayers.baselayers[newName])) { + var testBaseLayer = createLayer(newBaseLayers[newName]); + if (isDefined(testBaseLayer)) { + leafletLayers.baselayers[newName] = testBaseLayer; + + // Only add the visible layer to the map + if (newBaseLayers[newName].top === true) { + safeAddLayer(map, leafletLayers.baselayers[newName]); + } + } + } else { + if (newBaseLayers[newName].top === true && !map.hasLayer(leafletLayers.baselayers[newName])) { + safeAddLayer(map, leafletLayers.baselayers[newName]); + } else if (newBaseLayers[newName].top === false && map.hasLayer(leafletLayers.baselayers[newName])) { + map.removeLayer(leafletLayers.baselayers[newName]); + } + } + } + + //we have layers, so we need to make, at least, one active + var found = false; + + // search for an active layer + for (var key in leafletLayers.baselayers) { + if (map.hasLayer(leafletLayers.baselayers[key])) { + found = true; + break; + } + } + + // If there is no active layer make one active + if (!found && Object.keys(leafletLayers.baselayers).length > 0) { + safeAddLayer(map, leafletLayers.baselayers[Object.keys(leafletLayers.baselayers)[0]]); + } + + // Only show the layers switch selector control if we have more than one baselayer + overlay + isLayersControlVisible = updateLayersControl(map, mapId, isLayersControlVisible, newBaseLayers, layers.overlays, leafletLayers); + }, true); + + // Watch for the overlay layers + leafletScope.$watch('layers.overlays', function(newOverlayLayers, oldOverlayLayers) { + if (angular.equals(newOverlayLayers, oldOverlayLayers)) { + isLayersControlVisible = updateLayersControl(map, mapId, isLayersControlVisible, layers.baselayers, newOverlayLayers, leafletLayers); + return true; + } + + // Delete layers from the array + for (var name in leafletLayers.overlays) { + if (!isDefined(newOverlayLayers[name]) || newOverlayLayers[name].doRefresh) { + // Remove from the map if it's on it + if (map.hasLayer(leafletLayers.overlays[name])) { + // Safe remove when ArcGIS layers is loading. + var options = isDefined(newOverlayLayers[name]) ? + newOverlayLayers[name].layerOptions : null; + safeRemoveLayer(map, leafletLayers.overlays[name], options); + } + + // TODO: Depending on the layer type we will have to delete what's included on it + delete leafletLayers.overlays[name]; + + if (newOverlayLayers[name] && newOverlayLayers[name].doRefresh) { + newOverlayLayers[name].doRefresh = false; + } + } + } + + // add new overlays + for (var newName in newOverlayLayers) { + if (!isDefined(leafletLayers.overlays[newName])) { + var testOverlayLayer = createLayer(newOverlayLayers[newName]); + if (!isDefined(testOverlayLayer)) { + // If the layer creation fails, continue to the next overlay + continue; + } + + leafletLayers.overlays[newName] = testOverlayLayer; + if (newOverlayLayers[newName].visible === true) { + safeAddLayer(map, leafletLayers.overlays[newName]); + } + } else { + // check for the .visible property to hide/show overLayers + if (newOverlayLayers[newName].visible && !map.hasLayer(leafletLayers.overlays[newName])) { + safeAddLayer(map, leafletLayers.overlays[newName]); + } else if (newOverlayLayers[newName].visible === false && map.hasLayer(leafletLayers.overlays[newName])) { + // Safe remove when ArcGIS layers is loading. + safeRemoveLayer(map, leafletLayers.overlays[newName], newOverlayLayers[newName].layerOptions); + } + } + + //refresh heatmap data if present + if (newOverlayLayers[newName].visible && map._loaded && newOverlayLayers[newName].data && newOverlayLayers[newName].type === 'heatmap') { + leafletLayers.overlays[newName].setData(newOverlayLayers[newName].data); + leafletLayers.overlays[newName].update(); + } + } + + // Only add the layers switch selector control if we have more than one baselayer + overlay + isLayersControlVisible = updateLayersControl(map, mapId, isLayersControlVisible, layers.baselayers, newOverlayLayers, leafletLayers); + }, true); + }); + }, + }; +}]); + +angular.module('leaflet-directive').directive('legend', ["$log", "$http", "leafletHelpers", "leafletLegendHelpers", function($log, $http, leafletHelpers, leafletLegendHelpers) { + + return { + restrict: 'A', + scope: false, + replace: false, + require: 'leaflet', + + link: function(scope, element, attrs, controller) { + + var isArray = leafletHelpers.isArray; + var isDefined = leafletHelpers.isDefined; + var isFunction = leafletHelpers.isFunction; + var leafletScope = controller.getLeafletScope(); + var legend = leafletScope.legend; + + var legendClass; + var position; + var leafletLegend; + var type; + + leafletScope.$watch('legend', function(newLegend) { + + if (isDefined(newLegend)) { + + legendClass = newLegend.legendClass ? newLegend.legendClass : 'legend'; + + position = newLegend.position || 'bottomright'; + + // default to arcgis + type = newLegend.type || 'arcgis'; + } + + }, true); + + controller.getMap().then(function(map) { + + leafletScope.$watch('legend', function(newLegend) { + + if (!isDefined(newLegend)) { + + if (isDefined(leafletLegend)) { + leafletLegend.removeFrom(map); + leafletLegend = null; + } + + return; + } + + if (!isDefined(newLegend.url) && (type === 'arcgis') && (!isArray(newLegend.colors) || !isArray(newLegend.labels) || newLegend.colors.length !== newLegend.labels.length)) { + + $log.warn('[AngularJS - Leaflet] legend.colors and legend.labels must be set.'); + + return; + } + + if (isDefined(newLegend.url)) { + + $log.info('[AngularJS - Leaflet] loading legend service.'); + + return; + } + + if (isDefined(leafletLegend)) { + leafletLegend.removeFrom(map); + leafletLegend = null; + } + + leafletLegend = L.control({ + position: position, + }); + if (type === 'arcgis') { + leafletLegend.onAdd = leafletLegendHelpers.getOnAddArrayLegend(newLegend, legendClass); + } + + leafletLegend.addTo(map); + + }); + + leafletScope.$watch('legend.url', function(newURL) { + + if (!isDefined(newURL)) { + return; + } + + $http.get(newURL) + .success(function(legendData) { + + if (isDefined(leafletLegend)) { + + leafletLegendHelpers.updateLegend(leafletLegend.getContainer(), legendData, type, newURL); + + } else { + + leafletLegend = L.control({ + position: position, + }); + leafletLegend.onAdd = leafletLegendHelpers.getOnAddLegend(legendData, legendClass, type, newURL); + leafletLegend.addTo(map); + } + + if (isDefined(legend.loadedData) && isFunction(legend.loadedData)) { + legend.loadedData(); + } + }) + .error(function() { + $log.warn('[AngularJS - Leaflet] legend.url not loaded.'); + }); + }); + + }); + }, + }; +}]); + +angular.module('leaflet-directive').directive('markers', + ["$log", "$rootScope", "$q", "leafletData", "leafletHelpers", "leafletMapDefaults", "leafletMarkersHelpers", "leafletMarkerEvents", "leafletIterators", "leafletWatchHelpers", "leafletDirectiveControlsHelpers", function($log, $rootScope, $q, leafletData, leafletHelpers, leafletMapDefaults, + leafletMarkersHelpers, leafletMarkerEvents, leafletIterators, leafletWatchHelpers, + leafletDirectiveControlsHelpers) { + //less terse vars to helpers + var isDefined = leafletHelpers.isDefined; + var errorHeader = leafletHelpers.errorHeader; + var Helpers = leafletHelpers; + var isString = leafletHelpers.isString; + var addMarkerWatcher = leafletMarkersHelpers.addMarkerWatcher; + var updateMarker = leafletMarkersHelpers.updateMarker; + var listenMarkerEvents = leafletMarkersHelpers.listenMarkerEvents; + var addMarkerToGroup = leafletMarkersHelpers.addMarkerToGroup; + var createMarker = leafletMarkersHelpers.createMarker; + var deleteMarker = leafletMarkersHelpers.deleteMarker; + var $it = leafletIterators; + var _markersWatchOptions = leafletHelpers.watchOptions; + var maybeWatch = leafletWatchHelpers.maybeWatch; + var extendDirectiveControls = leafletDirectiveControlsHelpers.extend; + + var _getLMarker = function(leafletMarkers, name, maybeLayerName) { + if (!Object.keys(leafletMarkers).length) return; + if (maybeLayerName && isString(maybeLayerName)) { + if (!leafletMarkers[maybeLayerName] || !Object.keys(leafletMarkers[maybeLayerName]).length) + return; + return leafletMarkers[maybeLayerName][name]; + } + + return leafletMarkers[name]; + }; + + var _setLMarker = function(lObject, leafletMarkers, name, maybeLayerName) { + if (maybeLayerName && isString(maybeLayerName)) { + if (!isDefined(leafletMarkers[maybeLayerName])) + leafletMarkers[maybeLayerName] = {}; + leafletMarkers[maybeLayerName][name] = lObject; + } else + leafletMarkers[name] = lObject; + return lObject; + }; + + var _maybeAddMarkerToLayer = function(layerName, layers, model, marker, doIndividualWatch, map) { + + if (!isString(layerName)) { + $log.error(errorHeader + ' A layername must be a string'); + return false; + } + + if (!isDefined(layers)) { + $log.error(errorHeader + ' You must add layers to the directive if the markers are going to use this functionality.'); + return false; + } + + if (!isDefined(layers.overlays) || !isDefined(layers.overlays[layerName])) { + $log.error(errorHeader + ' A marker can only be added to a layer of type "group"'); + return false; + } + + var layerGroup = layers.overlays[layerName]; + if (!(layerGroup instanceof L.LayerGroup || layerGroup instanceof L.FeatureGroup)) { + $log.error(errorHeader + ' Adding a marker to an overlay needs a overlay of the type "group" or "featureGroup"'); + return false; + } + + // The marker goes to a correct layer group, so first of all we add it + layerGroup.addLayer(marker); + + // The marker is automatically added to the map depending on the visibility + // of the layer, so we only have to open the popup if the marker is in the map + if (!doIndividualWatch && map.hasLayer(marker) && model.focus === true) { + marker.openPopup(); + } + + return true; + }; + + //TODO: move to leafletMarkersHelpers??? or make a new class/function file (leafletMarkersHelpers is large already) + var _addMarkers = function(mapId, markersToRender, oldModels, map, layers, leafletMarkers, leafletScope, + watchOptions, maybeLayerName, skips) { + for (var newName in markersToRender) { + if (skips[newName]) + continue; + + if (newName.search('-') !== -1) { + $log.error('The marker can\'t use a "-" on his key name: "' + newName + '".'); + continue; + } + + var model = Helpers.copy(markersToRender[newName]); + var pathToMarker = Helpers.getObjectDotPath(maybeLayerName ? [maybeLayerName, newName] : [newName]); + var maybeLMarker = _getLMarker(leafletMarkers, newName, maybeLayerName); + if (!isDefined(maybeLMarker)) { + //(nmccready) very important to not have model changes when lObject is changed + //this might be desirable in some cases but it causes two-way binding to lObject which is not ideal + //if it is left as the reference then all changes from oldModel vs newModel are ignored + //see _destroy (where modelDiff becomes meaningless if we do not copy here) + var marker = createMarker(model); + var layerName = (model ? model.layer : undefined) || maybeLayerName; //original way takes pref + if (!isDefined(marker)) { + $log.error(errorHeader + ' Received invalid data on the marker ' + newName + '.'); + continue; + } + + _setLMarker(marker, leafletMarkers, newName, maybeLayerName); + + // Bind message + if (isDefined(model.message)) { + marker.bindPopup(model.message, model.popupOptions); + } + + // Add the marker to a cluster group if needed + if (isDefined(model.group)) { + var groupOptions = isDefined(model.groupOption) ? model.groupOption : null; + addMarkerToGroup(marker, model.group, groupOptions, map); + } + + // Show label if defined + if (Helpers.LabelPlugin.isLoaded() && isDefined(model.label) && isDefined(model.label.message)) { + marker.bindLabel(model.label.message, model.label.options); + } + + // Check if the marker should be added to a layer + if (isDefined(model) && (isDefined(model.layer) || isDefined(maybeLayerName))) { + + var pass = _maybeAddMarkerToLayer(layerName, layers, model, marker, + watchOptions.individual.doWatch, map); + if (!pass) + continue; //something went wrong move on in the loop + } else if (!isDefined(model.group)) { + // We do not have a layer attr, so the marker goes to the map layer + map.addLayer(marker); + if (!watchOptions.individual.doWatch && model.focus === true) { + marker.openPopup(); + } + } + + if (watchOptions.individual.doWatch) { + addMarkerWatcher(marker, pathToMarker, leafletScope, layers, map, + watchOptions.individual.isDeep); + } + + listenMarkerEvents(marker, model, leafletScope, watchOptions.individual.doWatch, map); + leafletMarkerEvents.bindEvents(mapId, marker, pathToMarker, model, leafletScope, layerName); + } else { + var oldModel = isDefined(oldModel) ? oldModels[newName] : undefined; + updateMarker(model, oldModel, maybeLMarker, pathToMarker, leafletScope, layers, map); + } + } + }; + + var _seeWhatWeAlreadyHave = function(markerModels, oldMarkerModels, lMarkers, isEqual, cb) { + var hasLogged = false; + var equals = false; + var oldMarker; + var newMarker; + + var doCheckOldModel = isDefined(oldMarkerModels); + for (var name in lMarkers) { + if (!hasLogged) { + $log.debug(errorHeader + '[markers] destroy: '); + hasLogged = true; + } + + if (doCheckOldModel) { + //might want to make the option (in watch options) to disable deep checking + //ie the options to only check !== (reference check) instead of angular.equals (slow) + newMarker = markerModels[name]; + oldMarker = oldMarkerModels[name]; + equals = angular.equals(newMarker, oldMarker) && isEqual; + } + + if (!isDefined(markerModels) || + !Object.keys(markerModels).length || + !isDefined(markerModels[name]) || + !Object.keys(markerModels[name]).length || + equals) { + if (cb && Helpers.isFunction(cb)) + cb(newMarker, oldMarker, name); + } + } + }; + + var _destroy = function(markerModels, oldMarkerModels, lMarkers, map, layers) { + _seeWhatWeAlreadyHave(markerModels, oldMarkerModels, lMarkers, false, + function(newMarker, oldMarker, lMarkerName) { + $log.debug(errorHeader + '[marker] is deleting marker: ' + lMarkerName); + deleteMarker(lMarkers[lMarkerName], map, layers); + delete lMarkers[lMarkerName]; + }); + }; + + var _getNewModelsToSkipp = function(newModels, oldModels, lMarkers) { + var skips = {}; + _seeWhatWeAlreadyHave(newModels, oldModels, lMarkers, true, + function(newMarker, oldMarker, lMarkerName) { + $log.debug(errorHeader + '[marker] is already rendered, marker: ' + lMarkerName); + skips[lMarkerName] = newMarker; + }); + + return skips; + }; + + return { + restrict: 'A', + scope: false, + replace: false, + require: ['leaflet', '?layers'], + + link: function(scope, element, attrs, controller) { + var mapController = controller[0]; + var leafletScope = mapController.getLeafletScope(); + + mapController.getMap().then(function(map) { + var leafletMarkers = {}; + var getLayers; + + // If the layers attribute is used, we must wait until the layers are created + if (isDefined(controller[1])) { + getLayers = controller[1].getLayers; + } else { + getLayers = function() { + var deferred = $q.defer(); + deferred.resolve(); + return deferred.promise; + }; + } + + var watchOptions = leafletScope.markersWatchOptions || _markersWatchOptions; + + // backwards compat + if (isDefined(attrs.watchMarkers)) + watchOptions.doWatch = watchOptions.individual.doWatch = + (!isDefined(attrs.watchMarkers) || Helpers.isTruthy(attrs.watchMarkers)); + + var isNested = (isDefined(attrs.markersNested) && Helpers.isTruthy(attrs.markersNested)); + + getLayers().then(function(layers) { + var _clean = function(models, oldModels) { + if (isNested) { + $it.each(models, function(markerToMaybeDel, layerName) { + var oldModel = isDefined(oldModel) ? oldModels[layerName] : undefined; + _destroy(markerToMaybeDel, oldModel, leafletMarkers[layerName], map, layers); + }); + + return; + } + + _destroy(models, oldModels, leafletMarkers, map, layers); + }; + + var _create = function(models, oldModels) { + _clean(models, oldModels); + var skips = null; + if (isNested) { + $it.each(models, function(markersToAdd, layerName) { + var oldModel = isDefined(oldModel) ? oldModels[layerName] : undefined; + skips = _getNewModelsToSkipp(models[layerName], oldModel, leafletMarkers[layerName]); + _addMarkers(attrs.id, markersToAdd, oldModels, map, layers, leafletMarkers, leafletScope, + watchOptions, layerName, skips); + }); + + return; + } + + skips = _getNewModelsToSkipp(models, oldModels, leafletMarkers); + _addMarkers(attrs.id, models, oldModels, map, layers, leafletMarkers, leafletScope, + watchOptions, undefined, skips); + }; + + extendDirectiveControls(attrs.id, 'markers', _create, _clean); + leafletData.setMarkers(leafletMarkers, attrs.id); + + maybeWatch(leafletScope, 'markers', watchOptions, function(newMarkers, oldMarkers) { + _create(newMarkers, oldMarkers); + }); + }); + }); + }, + }; + }]); + +angular.module('leaflet-directive').directive('maxbounds', ["$log", "leafletMapDefaults", "leafletBoundsHelpers", "leafletHelpers", function($log, leafletMapDefaults, leafletBoundsHelpers, leafletHelpers) { + + return { + restrict: 'A', + scope: false, + replace: false, + require: 'leaflet', + + link: function(scope, element, attrs, controller) { + var leafletScope = controller.getLeafletScope(); + var isValidBounds = leafletBoundsHelpers.isValidBounds; + var isNumber = leafletHelpers.isNumber; + + controller.getMap().then(function(map) { + leafletScope.$watch('maxbounds', function(maxbounds) { + if (!isValidBounds(maxbounds)) { + // Unset any previous maxbounds + map.setMaxBounds(); + return; + } + + var leafletBounds = leafletBoundsHelpers.createLeafletBounds(maxbounds); + if (isNumber(maxbounds.pad)) { + leafletBounds = leafletBounds.pad(maxbounds.pad); + } + + map.setMaxBounds(leafletBounds); + if (!attrs.center && !attrs.lfCenter) { + map.fitBounds(leafletBounds); + } + }); + }); + }, + }; +}]); + +angular.module('leaflet-directive').directive('paths', ["$log", "$q", "leafletData", "leafletMapDefaults", "leafletHelpers", "leafletPathsHelpers", "leafletPathEvents", function($log, $q, leafletData, leafletMapDefaults, leafletHelpers, leafletPathsHelpers, leafletPathEvents) { + + return { + restrict: 'A', + scope: false, + replace: false, + require: ['leaflet', '?layers'], + + link: function(scope, element, attrs, controller) { + var mapController = controller[0]; + var isDefined = leafletHelpers.isDefined; + var isString = leafletHelpers.isString; + var leafletScope = mapController.getLeafletScope(); + var paths = leafletScope.paths; + var createPath = leafletPathsHelpers.createPath; + var bindPathEvents = leafletPathEvents.bindPathEvents; + var setPathOptions = leafletPathsHelpers.setPathOptions; + + mapController.getMap().then(function(map) { + var defaults = leafletMapDefaults.getDefaults(attrs.id); + var getLayers; + + // If the layers attribute is used, we must wait until the layers are created + if (isDefined(controller[1])) { + getLayers = controller[1].getLayers; + } else { + getLayers = function() { + var deferred = $q.defer(); + deferred.resolve(); + return deferred.promise; + }; + } + + if (!isDefined(paths)) { + return; + } + + getLayers().then(function(layers) { + + var leafletPaths = {}; + leafletData.setPaths(leafletPaths, attrs.id); + + // Should we watch for every specific marker on the map? + var shouldWatch = (!isDefined(attrs.watchPaths) || attrs.watchPaths === 'true'); + + // Function for listening every single path once created + var watchPathFn = function(leafletPath, name) { + var clearWatch = leafletScope.$watch('paths["' + name + '"]', function(pathData, old) { + if (!isDefined(pathData)) { + if (isDefined(old.layer)) { + for (var i in layers.overlays) { + var overlay = layers.overlays[i]; + overlay.removeLayer(leafletPath); + } + } + + map.removeLayer(leafletPath); + clearWatch(); + return; + } + + setPathOptions(leafletPath, pathData.type, pathData); + }, true); + }; + + leafletScope.$watchCollection('paths', function(newPaths) { + + // Delete paths (by name) from the array + for (var name in leafletPaths) { + if (!isDefined(newPaths[name])) { + map.removeLayer(leafletPaths[name]); + delete leafletPaths[name]; + } + } + + // Create the new paths + for (var newName in newPaths) { + if (newName.search('\\$') === 0) { + continue; + } + + if (newName.search('-') !== -1) { + $log.error('[AngularJS - Leaflet] The path name "' + newName + '" is not valid. It must not include "-" and a number.'); + continue; + } + + if (!isDefined(leafletPaths[newName])) { + var pathData = newPaths[newName]; + var newPath = createPath(newName, newPaths[newName], defaults); + + // bind popup if defined + if (isDefined(newPath) && isDefined(pathData.message)) { + newPath.bindPopup(pathData.message, pathData.popupOptions); + } + + // Show label if defined + if (leafletHelpers.LabelPlugin.isLoaded() && isDefined(pathData.label) && isDefined(pathData.label.message)) { + newPath.bindLabel(pathData.label.message, pathData.label.options); + } + + // Check if the marker should be added to a layer + if (isDefined(pathData) && isDefined(pathData.layer)) { + + if (!isString(pathData.layer)) { + $log.error('[AngularJS - Leaflet] A layername must be a string'); + continue; + } + + if (!isDefined(layers)) { + $log.error('[AngularJS - Leaflet] You must add layers to the directive if the markers are going to use this functionality.'); + continue; + } + + if (!isDefined(layers.overlays) || !isDefined(layers.overlays[pathData.layer])) { + $log.error('[AngularJS - Leaflet] A path can only be added to a layer of type "group"'); + continue; + } + + var layerGroup = layers.overlays[pathData.layer]; + if (!(layerGroup instanceof L.LayerGroup || layerGroup instanceof L.FeatureGroup)) { + $log.error('[AngularJS - Leaflet] Adding a path to an overlay needs a overlay of the type "group" or "featureGroup"'); + continue; + } + + // Listen for changes on the new path + leafletPaths[newName] = newPath; + + // The path goes to a correct layer group, so first of all we add it + layerGroup.addLayer(newPath); + + if (shouldWatch) { + watchPathFn(newPath, newName); + } else { + setPathOptions(newPath, pathData.type, pathData); + } + } else if (isDefined(newPath)) { + // Listen for changes on the new path + leafletPaths[newName] = newPath; + map.addLayer(newPath); + + if (shouldWatch) { + watchPathFn(newPath, newName); + } else { + setPathOptions(newPath, pathData.type, pathData); + } + } + + bindPathEvents(attrs.id, newPath, newName, pathData, leafletScope); + } + } + }); + }); + }); + }, + }; +}]); + +angular.module('leaflet-directive').directive('tiles', ["$log", "leafletData", "leafletMapDefaults", "leafletHelpers", function($log, leafletData, leafletMapDefaults, leafletHelpers) { + + return { + restrict: 'A', + scope: false, + replace: false, + require: 'leaflet', + + link: function(scope, element, attrs, controller) { + var isDefined = leafletHelpers.isDefined; + var leafletScope = controller.getLeafletScope(); + var tiles = leafletScope.tiles; + + if (!isDefined(tiles) || !isDefined(tiles.url)) { + $log.warn('[AngularJS - Leaflet] The \'tiles\' definition doesn\'t have the \'url\' property.'); + return; + } + + controller.getMap().then(function(map) { + var defaults = leafletMapDefaults.getDefaults(attrs.id); + var tileLayerObj; + leafletScope.$watch('tiles', function(tiles, oldtiles) { + var tileLayerOptions = defaults.tileLayerOptions; + var tileLayerUrl = defaults.tileLayer; + + // If no valid tiles are in the scope, remove the last layer + if (!isDefined(tiles.url) && isDefined(tileLayerObj)) { + map.removeLayer(tileLayerObj); + return; + } + + // No leafletTiles object defined yet + if (!isDefined(tileLayerObj)) { + if (isDefined(tiles.options)) { + angular.copy(tiles.options, tileLayerOptions); + } + + if (isDefined(tiles.url)) { + tileLayerUrl = tiles.url; + } + + if (tiles.type === 'wms') { + tileLayerObj = L.tileLayer.wms(tileLayerUrl, tileLayerOptions); + } else { + tileLayerObj = L.tileLayer(tileLayerUrl, tileLayerOptions); + } + + tileLayerObj.addTo(map); + leafletData.setTiles(tileLayerObj, attrs.id); + return; + } + + // If the options of the tilelayer is changed, we need to redraw the layer + if (isDefined(tiles.url) && isDefined(tiles.options) && + (tiles.type !== oldtiles.type || !angular.equals(tiles.options, tileLayerOptions))) { + map.removeLayer(tileLayerObj); + tileLayerOptions = defaults.tileLayerOptions; + angular.copy(tiles.options, tileLayerOptions); + tileLayerUrl = tiles.url; + + if (tiles.type === 'wms') { + tileLayerObj = L.tileLayer.wms(tileLayerUrl, tileLayerOptions); + } else { + tileLayerObj = L.tileLayer(tileLayerUrl, tileLayerOptions); + } + + tileLayerObj.addTo(map); + leafletData.setTiles(tileLayerObj, attrs.id); + return; + } + + // Only the URL of the layer is changed, update the tiles object + if (isDefined(tiles.url)) { + tileLayerObj.setUrl(tiles.url); + } + }, true); + }); + }, + }; +}]); + +/* + Create multiple similar directives for watchOptions to support directiveControl + instead. (when watches are disabled) + NgAnnotate does not work here due to the functional creation +*/ +['markers', 'geojson'].forEach(function(name) { + angular.module('leaflet-directive').directive(name + 'WatchOptions', [ + '$log', '$rootScope', '$q', 'leafletData', 'leafletHelpers', + function($log, $rootScope, $q, leafletData, leafletHelpers) { + + var isDefined = leafletHelpers.isDefined, + errorHeader = leafletHelpers.errorHeader, + isObject = leafletHelpers.isObject, + _watchOptions = leafletHelpers.watchOptions; + + return { + restrict: 'A', + scope: false, + replace: false, + require: ['leaflet'], + + link: function(scope, element, attrs, controller) { + var mapController = controller[0], + leafletScope = mapController.getLeafletScope(); + + mapController.getMap().then(function() { + if (isDefined(scope[name + 'WatchOptions'])) { + if (isObject(scope[name + 'WatchOptions'])) + angular.extend(_watchOptions, scope[name + 'WatchOptions']); + else + $log.error(errorHeader + '[' + name + 'WatchOptions] is not an object'); + leafletScope[name + 'WatchOptions'] = _watchOptions; + } + }); + }, + }; + },]); +}); + +angular.module('leaflet-directive') +.factory('LeafletEventsHelpersFactory', ["$rootScope", "$q", "$log", "leafletHelpers", function($rootScope, $q, $log, leafletHelpers) { + var safeApply = leafletHelpers.safeApply; + var isDefined = leafletHelpers.isDefined; + var isObject = leafletHelpers.isObject; + var isArray = leafletHelpers.isArray; + var errorHeader = leafletHelpers.errorHeader; + + var EventsHelper = function(rootBroadcastName, lObjectType) { + this.rootBroadcastName = rootBroadcastName; + $log.debug('LeafletEventsHelpersFactory: lObjectType: ' + lObjectType + 'rootBroadcastName: ' + rootBroadcastName); + + //used to path/key out certain properties based on the type , "markers", "geojson" + this.lObjectType = lObjectType; + }; + + EventsHelper.prototype.getAvailableEvents = function() {return [];}; + + /* + argument: name: Note this can be a single string or dot notation + Example: + markerModel : { + m1: { lat:_, lon: _} + } + //would yield name of + name = "m1" + + If nested: + markerModel : { + cars: { + m1: { lat:_, lon: _} + } + } + //would yield name of + name = "cars.m1" + */ + EventsHelper.prototype.genDispatchEvent = function(maybeMapId, eventName, logic, leafletScope, lObject, name, model, layerName, extra) { + var _this = this; + + maybeMapId = maybeMapId || ''; + if (maybeMapId) + maybeMapId = '.' + maybeMapId; + + return function(e) { + var broadcastName = _this.rootBroadcastName + maybeMapId + '.' + eventName; + $log.debug(broadcastName); + _this.fire(leafletScope, broadcastName, logic, e, e.target || lObject, model, name, layerName, extra); + }; + }; + + EventsHelper.prototype.fire = function(scope, broadcastName, logic, event, lObject, model, modelName, layerName) { + // Safely broadcast the event + safeApply(scope, function() { + var toSend = { + leafletEvent: event, + leafletObject: lObject, + modelName: modelName, + model: model, + }; + if (isDefined(layerName)) + angular.extend(toSend, {layerName: layerName}); + + if (logic === 'emit') { + scope.$emit(broadcastName, toSend); + } else { + $rootScope.$broadcast(broadcastName, toSend); + } + }); + }; + + EventsHelper.prototype.bindEvents = function(maybeMapId, lObject, name, model, leafletScope, layerName, extra) { + var events = []; + var logic = 'emit'; + var _this = this; + + if (!isDefined(leafletScope.eventBroadcast)) { + // Backward compatibility, if no event-broadcast attribute, all events are broadcasted + events = this.getAvailableEvents(); + } else if (!isObject(leafletScope.eventBroadcast)) { + // Not a valid object + $log.error(errorHeader + 'event-broadcast must be an object check your model.'); + } else { + // We have a possible valid object + if (!isDefined(leafletScope.eventBroadcast[_this.lObjectType])) { + // We do not have events enable/disable do we do nothing (all enabled by default) + events = this.getAvailableEvents(); + } else if (!isObject(leafletScope.eventBroadcast[_this.lObjectType])) { + // Not a valid object + $log.warn(errorHeader + 'event-broadcast.' + [_this.lObjectType] + ' must be an object check your model.'); + } else { + // We have a possible valid map object + // Event propadation logic + if (isDefined(leafletScope.eventBroadcast[this.lObjectType].logic)) { + // We take care of possible propagation logic + if (leafletScope.eventBroadcast[_this.lObjectType].logic !== 'emit' && + leafletScope.eventBroadcast[_this.lObjectType].logic !== 'broadcast') + $log.warn(errorHeader + 'Available event propagation logic are: \'emit\' or \'broadcast\'.'); + } + + // Enable / Disable + var eventsEnable = false; + var eventsDisable = false; + if (isDefined(leafletScope.eventBroadcast[_this.lObjectType].enable) && + isArray(leafletScope.eventBroadcast[_this.lObjectType].enable)) + eventsEnable = true; + if (isDefined(leafletScope.eventBroadcast[_this.lObjectType].disable) && + isArray(leafletScope.eventBroadcast[_this.lObjectType].disable)) + eventsDisable = true; + + if (eventsEnable && eventsDisable) { + // Both are active, this is an error + $log.warn(errorHeader + 'can not enable and disable events at the same time'); + } else if (!eventsEnable && !eventsDisable) { + // Both are inactive, this is an error + $log.warn(errorHeader + 'must enable or disable events'); + } else { + // At this point the object is OK, lets enable or disable events + if (eventsEnable) { + // Enable events + leafletScope.eventBroadcast[this.lObjectType].enable.forEach(function(eventName) { + // Do we have already the event enabled? + if (events.indexOf(eventName) !== -1) { + // Repeated event, this is an error + $log.warn(errorHeader + 'This event ' + eventName + ' is already enabled'); + } else { + // Does the event exists? + if (_this.getAvailableEvents().indexOf(eventName) === -1) { + // The event does not exists, this is an error + $log.warn(errorHeader + 'This event ' + eventName + ' does not exist'); + } else { + // All ok enable the event + events.push(eventName); + } + } + }); + } else { + // Disable events + events = this.getAvailableEvents(); + leafletScope.eventBroadcast[_this.lObjectType].disable.forEach(function(eventName) { + var index = events.indexOf(eventName); + if (index === -1) { + // The event does not exist + $log.warn(errorHeader + 'This event ' + eventName + ' does not exist or has been already disabled'); + + } else { + events.splice(index, 1); + } + }); + } + } + } + } + + events.forEach(function(eventName) { + lObject.on(eventName, _this.genDispatchEvent(maybeMapId, eventName, logic, leafletScope, lObject, name, model, layerName, extra)); + }); + + return logic; + }; + + return EventsHelper; +}]) +.service('leafletEventsHelpers', ["LeafletEventsHelpersFactory", function(LeafletEventsHelpersFactory) { + return new LeafletEventsHelpersFactory(); +}]); + +angular.module('leaflet-directive') +.factory('leafletGeoJsonEvents', ["$rootScope", "$q", "$log", "leafletHelpers", "LeafletEventsHelpersFactory", "leafletData", function($rootScope, $q, $log, leafletHelpers, + LeafletEventsHelpersFactory, leafletData) { + var safeApply = leafletHelpers.safeApply; + var EventsHelper = LeafletEventsHelpersFactory; + + var GeoJsonEvents = function() { + EventsHelper.call(this, 'leafletDirectiveGeoJson', 'geojson'); + }; + + GeoJsonEvents.prototype = new EventsHelper(); + + GeoJsonEvents.prototype.genDispatchEvent = function(maybeMapId, eventName, logic, leafletScope, lObject, name, model, layerName, extra) { + var base = EventsHelper.prototype.genDispatchEvent.call(this, maybeMapId, eventName, logic, leafletScope, lObject, name, model, layerName); + var _this = this; + + return function(e) { + if (eventName === 'mouseout') { + if (extra.resetStyleOnMouseout) { + leafletData.getGeoJSON(extra.mapId) + .then(function(leafletGeoJSON) { + //this is broken on nested needs to traverse or user layerName (nested) + var lobj = layerName ? leafletGeoJSON[layerName] : leafletGeoJSON; + lobj.resetStyle(e.target); + }); + + } + + safeApply(leafletScope, function() { + $rootScope.$broadcast(_this.rootBroadcastName + '.mouseout', e); + }); + } + + base(e); //common + }; + }; + + GeoJsonEvents.prototype.getAvailableEvents = function() { return [ + 'click', + 'dblclick', + 'mouseover', + 'mouseout', + ]; + }; + + return new GeoJsonEvents(); +}]); + +angular.module('leaflet-directive') +.factory('leafletLabelEvents', ["$rootScope", "$q", "$log", "leafletHelpers", "LeafletEventsHelpersFactory", function($rootScope, $q, $log, leafletHelpers, LeafletEventsHelpersFactory) { + var Helpers = leafletHelpers; + var EventsHelper = LeafletEventsHelpersFactory; + + var LabelEvents = function() { + EventsHelper.call(this, 'leafletDirectiveLabel', 'markers'); + }; + + LabelEvents.prototype = new EventsHelper(); + + LabelEvents.prototype.genDispatchEvent = function(maybeMapId, eventName, logic, leafletScope, lObject, name, model, layerName) { + var markerName = name.replace('markers.', ''); + return EventsHelper.prototype + .genDispatchEvent.call(this, maybeMapId, eventName, logic, leafletScope, lObject, markerName, model, layerName); + }; + + LabelEvents.prototype.getAvailableEvents = function() { + return [ + 'click', + 'dblclick', + 'mousedown', + 'mouseover', + 'mouseout', + 'contextmenu', + ]; + }; + + LabelEvents.prototype.genEvents = function(maybeMapId, eventName, logic, leafletScope, lObject, name, model, layerName) { + var _this = this; + var labelEvents = this.getAvailableEvents(); + var scopeWatchName = Helpers.getObjectArrayPath('markers.' + name); + labelEvents.forEach(function(eventName) { + lObject.label.on(eventName, _this.genDispatchEvent( + maybeMapId, eventName, logic, leafletScope, lObject.label, scopeWatchName, model, layerName)); + }); + }; + + LabelEvents.prototype.bindEvents = function() {}; + + return new LabelEvents(); +}]); + +angular.module('leaflet-directive') +.factory('leafletMapEvents', ["$rootScope", "$q", "$log", "leafletHelpers", "leafletEventsHelpers", "leafletIterators", function($rootScope, $q, $log, leafletHelpers, leafletEventsHelpers, leafletIterators) { + var isDefined = leafletHelpers.isDefined; + var fire = leafletEventsHelpers.fire; + + var _getAvailableMapEvents = function() { + return [ + 'click', + 'dblclick', + 'mousedown', + 'mouseup', + 'mouseover', + 'mouseout', + 'mousemove', + 'contextmenu', + 'focus', + 'blur', + 'preclick', + 'load', + 'unload', + 'viewreset', + 'movestart', + 'move', + 'moveend', + 'dragstart', + 'drag', + 'dragend', + 'zoomstart', + 'zoomanim', + 'zoomend', + 'zoomlevelschange', + 'resize', + 'autopanstart', + 'layeradd', + 'layerremove', + 'baselayerchange', + 'overlayadd', + 'overlayremove', + 'locationfound', + 'locationerror', + 'popupopen', + 'popupclose', + 'draw:created', + 'draw:edited', + 'draw:deleted', + 'draw:drawstart', + 'draw:drawstop', + 'draw:editstart', + 'draw:editstop', + 'draw:deletestart', + 'draw:deletestop', + ]; + }; + + var _genDispatchMapEvent = function(scope, eventName, logic, maybeMapId) { + if (maybeMapId) + maybeMapId = maybeMapId + '.'; + return function(e) { + // Put together broadcast name + var broadcastName = 'leafletDirectiveMap.' + maybeMapId + eventName; + $log.debug(broadcastName); + + // Safely broadcast the event + fire(scope, broadcastName, logic, e, e.target, scope); + }; + }; + + var _notifyCenterChangedToBounds = function(scope) { + scope.$broadcast('boundsChanged'); + }; + + var _notifyCenterUrlHashChanged = function(scope, map, attrs, search) { + if (!isDefined(attrs.urlHashCenter)) { + return; + } + + var center = map.getCenter(); + var centerUrlHash = (center.lat).toFixed(4) + ':' + (center.lng).toFixed(4) + ':' + map.getZoom(); + if (!isDefined(search.c) || search.c !== centerUrlHash) { + //$log.debug("notified new center..."); + scope.$emit('centerUrlHash', centerUrlHash); + } + }; + + var _addEvents = function(map, mapEvents, contextName, scope, logic) { + leafletIterators.each(mapEvents, function(eventName) { + var context = {}; + context[contextName] = eventName; + map.on(eventName, _genDispatchMapEvent(scope, eventName, logic, map._container.id || ''), context); + }); + }; + + return { + getAvailableMapEvents: _getAvailableMapEvents, + genDispatchMapEvent: _genDispatchMapEvent, + notifyCenterChangedToBounds: _notifyCenterChangedToBounds, + notifyCenterUrlHashChanged: _notifyCenterUrlHashChanged, + addEvents: _addEvents, + }; +}]); + +angular.module('leaflet-directive') +.factory('leafletMarkerEvents', ["$rootScope", "$q", "$log", "leafletHelpers", "LeafletEventsHelpersFactory", "leafletLabelEvents", function($rootScope, $q, $log, leafletHelpers, LeafletEventsHelpersFactory, leafletLabelEvents) { + var safeApply = leafletHelpers.safeApply; + var isDefined = leafletHelpers.isDefined; + var Helpers = leafletHelpers; + var lblHelp = leafletLabelEvents; + var EventsHelper = LeafletEventsHelpersFactory; + + var MarkerEvents = function() { + EventsHelper.call(this, 'leafletDirectiveMarker', 'markers'); + }; + + MarkerEvents.prototype = new EventsHelper(); + + MarkerEvents.prototype.genDispatchEvent = function(maybeMapId, eventName, logic, leafletScope, lObject, name, model, layerName) { + var handle = EventsHelper.prototype + .genDispatchEvent.call(this, maybeMapId, eventName, logic, leafletScope, lObject, name, model, layerName); + return function(e) { + // Broadcast old marker click name for backwards compatibility + if (eventName === 'click') { + safeApply(leafletScope, function() { + $rootScope.$broadcast('leafletDirectiveMarkersClick', name); + }); + } else if (eventName === 'dragend') { + safeApply(leafletScope, function() { + model.lat = lObject.getLatLng().lat; + model.lng = lObject.getLatLng().lng; + }); + + if (model.message && model.focus === true) { + lObject.openPopup(); + } + } + + handle(e); //common + }; + }; + + MarkerEvents.prototype.getAvailableEvents = function() { return [ + 'click', + 'dblclick', + 'mousedown', + 'mouseover', + 'mouseout', + 'contextmenu', + 'dragstart', + 'drag', + 'dragend', + 'move', + 'remove', + 'popupopen', + 'popupclose', + 'touchend', + 'touchstart', + 'touchmove', + 'touchcancel', + 'touchleave', + ]; + }; + + MarkerEvents.prototype.bindEvents = function(maybeMapId, lObject, name, model, leafletScope, layerName) { + var logic = EventsHelper.prototype.bindEvents.call(this, maybeMapId, lObject, name, model, leafletScope, layerName); + + if (Helpers.LabelPlugin.isLoaded() && isDefined(lObject.label)) { + lblHelp.genEvents(maybeMapId, name, logic, leafletScope, lObject, model, layerName); + } + }; + + return new MarkerEvents(); +}]); + +angular.module('leaflet-directive') +.factory('leafletPathEvents', ["$rootScope", "$q", "$log", "leafletHelpers", "leafletLabelEvents", "leafletEventsHelpers", function($rootScope, $q, $log, leafletHelpers, leafletLabelEvents, leafletEventsHelpers) { + var isDefined = leafletHelpers.isDefined; + var isObject = leafletHelpers.isObject; + var Helpers = leafletHelpers; + var errorHeader = leafletHelpers.errorHeader; + var lblHelp = leafletLabelEvents; + var fire = leafletEventsHelpers.fire; + + /* + TODO (nmccready) This EventsHelper needs to be derrived from leafletEventsHelpers to elminate copy and paste code. + */ + + var _genDispatchPathEvent = function(maybeMapId, eventName, logic, leafletScope, lObject, name, model, layerName) { + maybeMapId = maybeMapId || ''; + + if (maybeMapId) + maybeMapId = '.' + maybeMapId; + + return function(e) { + var broadcastName = 'leafletDirectivePath' + maybeMapId + '.' + eventName; + $log.debug(broadcastName); + fire(leafletScope, broadcastName, logic, e, e.target || lObject, model, name, layerName); + }; + }; + + var _bindPathEvents = function(maybeMapId, lObject, name, model, leafletScope) { + var pathEvents = []; + var i; + var eventName; + var logic = 'broadcast'; + + if (!isDefined(leafletScope.eventBroadcast)) { + // Backward compatibility, if no event-broadcast attribute, all events are broadcasted + pathEvents = _getAvailablePathEvents(); + } else if (!isObject(leafletScope.eventBroadcast)) { + // Not a valid object + $log.error(errorHeader + 'event-broadcast must be an object check your model.'); + } else { + // We have a possible valid object + if (!isDefined(leafletScope.eventBroadcast.path)) { + // We do not have events enable/disable do we do nothing (all enabled by default) + pathEvents = _getAvailablePathEvents(); + } else if (isObject(leafletScope.eventBroadcast.paths)) { + // Not a valid object + $log.warn(errorHeader + 'event-broadcast.path must be an object check your model.'); + } else { + // We have a possible valid map object + // Event propadation logic + if (leafletScope.eventBroadcast.path.logic !== undefined && leafletScope.eventBroadcast.path.logic !== null) { + // We take care of possible propagation logic + if (leafletScope.eventBroadcast.path.logic !== 'emit' && leafletScope.eventBroadcast.path.logic !== 'broadcast') { + // This is an error + $log.warn(errorHeader + 'Available event propagation logic are: \'emit\' or \'broadcast\'.'); + } else if (leafletScope.eventBroadcast.path.logic === 'emit') { + logic = 'emit'; + } + } + + // Enable / Disable + var pathEventsEnable = false; + var pathEventsDisable = false; + if (leafletScope.eventBroadcast.path.enable !== undefined && leafletScope.eventBroadcast.path.enable !== null) { + if (typeof leafletScope.eventBroadcast.path.enable === 'object') { + pathEventsEnable = true; + } + } + + if (leafletScope.eventBroadcast.path.disable !== undefined && leafletScope.eventBroadcast.path.disable !== null) { + if (typeof leafletScope.eventBroadcast.path.disable === 'object') { + pathEventsDisable = true; + } + } + + if (pathEventsEnable && pathEventsDisable) { + // Both are active, this is an error + $log.warn(errorHeader + 'can not enable and disable events at the same time'); + } else if (!pathEventsEnable && !pathEventsDisable) { + // Both are inactive, this is an error + $log.warn(errorHeader + 'must enable or disable events'); + } else { + // At this point the path object is OK, lets enable or disable events + if (pathEventsEnable) { + // Enable events + for (i = 0; i < leafletScope.eventBroadcast.path.enable.length; i++) { + eventName = leafletScope.eventBroadcast.path.enable[i]; + + // Do we have already the event enabled? + if (pathEvents.indexOf(eventName) !== -1) { + // Repeated event, this is an error + $log.warn(errorHeader + 'This event ' + eventName + ' is already enabled'); + } else { + // Does the event exists? + if (_getAvailablePathEvents().indexOf(eventName) === -1) { + // The event does not exists, this is an error + $log.warn(errorHeader + 'This event ' + eventName + ' does not exist'); + } else { + // All ok enable the event + pathEvents.push(eventName); + } + } + } + } else { + // Disable events + pathEvents = _getAvailablePathEvents(); + for (i = 0; i < leafletScope.eventBroadcast.path.disable.length; i++) { + eventName = leafletScope.eventBroadcast.path.disable[i]; + var index = pathEvents.indexOf(eventName); + if (index === -1) { + // The event does not exist + $log.warn(errorHeader + 'This event ' + eventName + ' does not exist or has been already disabled'); + + } else { + pathEvents.splice(index, 1); + } + } + } + } + } + } + + for (i = 0; i < pathEvents.length; i++) { + eventName = pathEvents[i]; + lObject.on(eventName, _genDispatchPathEvent(maybeMapId, eventName, logic, leafletScope, pathEvents, name)); + } + + if (Helpers.LabelPlugin.isLoaded() && isDefined(lObject.label)) { + lblHelp.genEvents(maybeMapId, name, logic, leafletScope, lObject, model); + } + }; + + var _getAvailablePathEvents = function() { + return [ + 'click', + 'dblclick', + 'mousedown', + 'mouseover', + 'mouseout', + 'contextmenu', + 'add', + 'remove', + 'popupopen', + 'popupclose', + ]; + }; + + return { + getAvailablePathEvents: _getAvailablePathEvents, + bindPathEvents: _bindPathEvents, + }; +}]); + +}(angular)); \ No newline at end of file diff --git a/app/bower_components/angular-leaflet-directive/generate-examples.js b/app/bower_components/angular-leaflet-directive/generate-examples.js new file mode 100644 index 0000000..40abdfd --- /dev/null +++ b/app/bower_components/angular-leaflet-directive/generate-examples.js @@ -0,0 +1,205 @@ +#!/usr/bin/env node + +'use strict'; + +var fs = require('fs'); +var path = require('path'); +var mkdirp = require('mkdirp'); +var jsdom = require('jsdom'); +var Q = require('q'); + +var onlyStandAlone = [ + "0117-basic-routing-show-hide-map-example.html" +]; + +var isAnExample = function(filename) { + if (filename === '0000-viewer.html') { + return false; + } + return /[0-9][0-9][0-9][0-9].*\.html/.test(filename); +}; + +var isJavascript = function(filename) { + return /.*\.js/.test(filename); +}; + +var deleteFileIfJavascript = function(filename) { + var df = Q.defer(); + if (isJavascript(filename)) { + fs.unlink(filename, function() { + df.resolve(); + }); + } else { + df.resolve(); + } + + return df.promise; +}; + +var cleanJavascriptFilesFromControllersDirectory = function(dir) { + var df = Q.defer(); + fs.readdir(dir, function(err, list) { + var l = []; + var files = list.map(function(file) { + return path.join(dir, file); + }); + files.forEach(deleteFileIfJavascript); + Q.allSettled(l).then(function(result) { + df.resolve(); + }); + }); + return df.promise; +}; + +var writeController = function(script, examplefile, controllers_directory) { + var df = Q.defer(); + var scriptLines = script.split('\n'); + var outfilename; + var outScript = []; + for (var i = 0; i < scriptLines.length; i++) { + + var line = scriptLines[i]; + + // Remove empty lines + if (line.replace(/^\s+|\s+$/g, '') === '') { + continue; + } + + // Remove the module initializacion line + if (line.search('angular.module') !== -1) { + continue; + } + + // Extract controller name + if (line.search('app.controller') !== -1 && !outfilename) { + var controller = line.match(/app.controller\((\'|\")([^'"]*)/); + if (controller && controller.length > 2 && controller[2]) { + outfilename = controller[2] + '.js'; + } + } + + outScript.push(line); + } + + if (outfilename) { + outfilename = path.join(controllers_directory, outfilename); + if (!fs.existsSync(outfilename)) { + fs.writeFile(outfilename, outScript.join('\n'), function() { + df.resolve(); + }); + } else { + console.log('The controller name is duplicated: ' + outfilename) + df.reject(); + } + } else { + console.log('Can\'t identify the controller name in the example ' + examplefile) + df.reject(); + } + + return df.promise; +}; + +var generateControllersFromExamples = function(examples_directory, controllers_directory) { + var df = Q.defer(); + fs.readdir(examples_directory, function(err, list) { + var l = []; + list.forEach(function(filename) { + if (isAnExample(filename)) { + var html = fs.readFileSync(path.join(__dirname, 'examples', filename)); + jsdom.env({ + html: html.toString(), + done: function(err, window) { + var scripts = window.document.getElementsByTagName('script'); + var last = scripts.length -1; + var script = scripts[last].innerHTML; + l.push(writeController(script, filename, controllers_directory)); + } + }); + } + }); + + Q.allSettled(l).then(function() { + df.resolve(); + }); + }); + + return df.promise; +}; + +var extractId = function(filename) { + var arr = filename.replace('.html', '').split('-'); + arr.splice(0,2); + return arr.join('-'); +}; + +var extractTitle = function(filename) { + var html = fs.readFileSync(path.join(__dirname, 'examples', filename)); + var title; + var arr = html.toString().split('\n'); + + for (var i = 0; i< arr.length; i++) { + var line = arr[i]; + + if (line.search('

        ') !== -1) { + title = line.replace('

        ', '').replace('

        ', '').replace(/^ */, ''); + } + } + + return title; +}; + +var extractDescription = function(filename) { + +}; + +var extractDate = function(filename) { + var stats = fs.statSync(filename); + return stats.mtime; +}; + +var generateExamplesJSONFile = function(examples_directory, json_file) { + var df = Q.defer(); + var examples = {}; + fs.readdir(examples_directory, function(err, list) { + list.forEach(function(filename) { + if (isAnExample(filename)) { + var section = filename.split('-')[1]; + var id = extractId(filename); + var extUrl = filename; + var title = extractTitle(filename); + var description = extractDescription(filename); + var date = extractDate(path.join(examples_directory, filename)); + + if (!(section in examples)) { + examples[section] = []; + } + examples[section].push({ + date: date, + section: section, + onlyStandAlone: onlyStandAlone.indexOf(extUrl) !== -1, + id: '/' + section + '/' + id, + extUrl: extUrl, + title: title, + description: description + }); + } + }); + + fs.writeFile(json_file, JSON.stringify(examples, null, 4), function(err) { + df.resolve(); + }); + }); + + return df.promise; +}; + +var controllers_directory = path.join(__dirname, 'examples', 'js', 'controllers'); +mkdirp(controllers_directory, function(err) { + cleanJavascriptFilesFromControllersDirectory(controllers_directory).then(function() { + var examples_directory = path.join(__dirname, 'examples'); + generateControllersFromExamples(examples_directory, controllers_directory).then(function() { + var json_file = path.join(__dirname, 'examples', 'json', 'examples.json'); + generateExamplesJSONFile(examples_directory, json_file); + }); + }); +}); diff --git a/app/bower_components/angular-leaflet-directive/grunt/aliases.yaml b/app/bower_components/angular-leaflet-directive/grunt/aliases.yaml new file mode 100644 index 0000000..73d3a46 --- /dev/null +++ b/app/bower_components/angular-leaflet-directive/grunt/aliases.yaml @@ -0,0 +1,54 @@ +test: + - 'jshint' + - 'test-unit' + - 'test-e2e' + +test-unit: + - 'karma:unit' + +test-e2e: + - 'shell:protractor_update' + - 'connect:testserver' + - 'protractor:run' + +test-e2e-firefox: + - 'shell:protractor_update' + - 'connect:testserver' + - 'protractor:firefox' + +coverage: + - 'karma:unit_coverage' + - 'open:coverage' + - 'connect:coverage' + +install: + - 'shell:npm_install' + - 'bower:install' + - 'shell:protractor_update' + +default: + - 'build' + +fast-build: + - 'clean:dist' + - 'jshint' + - 'jscs' + - 'concat:dist' + - 'ngAnnotate' + - 'uglify' + - 'concat:license' + - 'concat:license-minified' + +build: + - 'fast-build' + - 'test-unit' + - 'clean:pre' + +travis: + - 'fast-build' + - 'bower:install' + - 'test-unit' + +examples: + - 'shell:examples' + - 'concat:examples' diff --git a/app/bower_components/angular-leaflet-directive/grunt/clean.json b/app/bower_components/angular-leaflet-directive/grunt/clean.json new file mode 100644 index 0000000..acaec86 --- /dev/null +++ b/app/bower_components/angular-leaflet-directive/grunt/clean.json @@ -0,0 +1,4 @@ +{ + "dist": ["dist"], + "pre": ["dist/*.pre.js"] +} diff --git a/app/bower_components/angular-leaflet-directive/grunt/concat.json b/app/bower_components/angular-leaflet-directive/grunt/concat.json new file mode 100644 index 0000000..cbb6224 --- /dev/null +++ b/app/bower_components/angular-leaflet-directive/grunt/concat.json @@ -0,0 +1,36 @@ +{ + "dist": { + "options": { + "banner": "/*!\n* <%= pkg.name %> <%= pkg.version %> <%= grunt.template.today(\"yyyy-mm-dd\") %>\n* <%= pkg.description %>\n* <%= pkg.repository.type %>: <%= pkg.repository.url %>\n*/\n(function(angular){\n'use strict';\n", + "footer": "\n}(angular));" + }, + "src": [ + "src/directives/leaflet.js", + "src/services/*.js", + "src/**/*.js" + ], + "dest": "dist/angular-leaflet-directive.pre.js" + }, + "license": { + "src": [ + "src/header-MIT-license.txt", + "dist/angular-leaflet-directive.no-header.js" + ], + "dest": "dist/angular-leaflet-directive.js" + }, + "license-minified": { + "src": [ + "src/header-MIT-license.txt", + "dist/angular-leaflet-directive.min.no-header.js" + ], + "dest": "dist/angular-leaflet-directive.min.js" + }, + "examples": { + "options": { + "banner": "(function(angular){ \nvar app = angular.module('webapp');\n", + "footer": "}(angular));" + }, + "src": ["examples/js/controllers/*.js"], + "dest": "examples/js/controllers.js" + } +} diff --git a/app/bower_components/angular-leaflet-directive/grunt/connect.json b/app/bower_components/angular-leaflet-directive/grunt/connect.json new file mode 100644 index 0000000..c051f8a --- /dev/null +++ b/app/bower_components/angular-leaflet-directive/grunt/connect.json @@ -0,0 +1,18 @@ +{ + "options": { + "base": "" + }, + "testserver": { + "options": { + "port": 9999 + } + }, + "coverage": { + "options": { + "base": "coverage/", + "directory": "coverage/", + "port": 5555, + "keepalive": true + } + } +} diff --git a/app/bower_components/angular-leaflet-directive/grunt/jscs.json b/app/bower_components/angular-leaflet-directive/grunt/jscs.json new file mode 100644 index 0000000..7999fff --- /dev/null +++ b/app/bower_components/angular-leaflet-directive/grunt/jscs.json @@ -0,0 +1,7 @@ + { + "src": ["src/directives/*.js", "src/services/**/*.js"], + "options": { + "config": ".jscsrc", + "fix": true + } +} diff --git a/app/bower_components/angular-leaflet-directive/grunt/jshint.json b/app/bower_components/angular-leaflet-directive/grunt/jshint.json new file mode 100644 index 0000000..0ddc818 --- /dev/null +++ b/app/bower_components/angular-leaflet-directive/grunt/jshint.json @@ -0,0 +1,14 @@ + { + "options": { + "jshintrc": true + }, + "source": { + "src": ["src/directives/*.js", "src/services/**/*.js"] + }, + "tests": { + "src": ["test/unit/*.js", "test/e2e/*.js"] + }, + "grunt": { + "src": ["Gruntfile.js"] + } +} diff --git a/app/bower_components/angular-leaflet-directive/grunt/karma.json b/app/bower_components/angular-leaflet-directive/grunt/karma.json new file mode 100644 index 0000000..daec2cb --- /dev/null +++ b/app/bower_components/angular-leaflet-directive/grunt/karma.json @@ -0,0 +1,7 @@ +{ + "unit": { + "configFile": "test/karma-unit.conf.js", + "autoWatch": false, + "singleRun": true + } +} diff --git a/app/bower_components/angular-leaflet-directive/grunt/ngAnnotate.json b/app/bower_components/angular-leaflet-directive/grunt/ngAnnotate.json new file mode 100644 index 0000000..2522a73 --- /dev/null +++ b/app/bower_components/angular-leaflet-directive/grunt/ngAnnotate.json @@ -0,0 +1,8 @@ +{ + "options": {}, + "dist": { + "files": { + "dist/angular-leaflet-directive.no-header.js": [ "dist/angular-leaflet-directive.pre.js" ] + } + } +} diff --git a/app/bower_components/angular-leaflet-directive/grunt/open.json b/app/bower_components/angular-leaflet-directive/grunt/open.json new file mode 100644 index 0000000..9196e9c --- /dev/null +++ b/app/bower_components/angular-leaflet-directive/grunt/open.json @@ -0,0 +1,8 @@ +{ + "devserver": { + "path": "http://localhost:8888" + }, + "coverage": { + "path": "http://localhost:5555" + } +} diff --git a/app/bower_components/angular-leaflet-directive/grunt/protractor.json b/app/bower_components/angular-leaflet-directive/grunt/protractor.json new file mode 100644 index 0000000..7d4c8d5 --- /dev/null +++ b/app/bower_components/angular-leaflet-directive/grunt/protractor.json @@ -0,0 +1,20 @@ +{ + "options": { + "keepAlive": false, + "configFile": "test/protractor.conf.js", + "nocolor": false, + "args": { + "specs": [ "test/e2e/*.js" ] + } + }, + "run": {}, + "saucelabs": { + "options": { + "args": { + "baseUrl": "http://tombatossals.github.io/angular-leaflet-directive/examples/", + "sauceUser": "<%= saucelabs.SAUCE_USERNAME %>", + "sauceKey": "<% saucelabs.SAUCE_ACCESS_KEY %>" + } + } + } +} diff --git a/app/bower_components/angular-leaflet-directive/grunt/shell.json b/app/bower_components/angular-leaflet-directive/grunt/shell.json new file mode 100644 index 0000000..9774b67 --- /dev/null +++ b/app/bower_components/angular-leaflet-directive/grunt/shell.json @@ -0,0 +1,21 @@ + { + "options": { + "stdout": true + }, + "selenium": { + "command": "node node_modules/protractor/bin/webdriver-manager start", + "options": { + "stdout": false, + "async": true + } + }, + "protractor_update": { + "command": "node node_modules/protractor/bin/webdriver-manager update" + }, + "npm_install": { + "command": "npm install" + }, + "examples": { + "command": "node generate-examples.js" + } +} diff --git a/app/bower_components/angular-leaflet-directive/grunt/uglify.json b/app/bower_components/angular-leaflet-directive/grunt/uglify.json new file mode 100644 index 0000000..f53aa71 --- /dev/null +++ b/app/bower_components/angular-leaflet-directive/grunt/uglify.json @@ -0,0 +1,11 @@ +{ + "dist": { + "options": { + "banner": "/*!\n* <%= pkg.name %> <%= pkg.version %> <%= grunt.template.today(\"yyyy-mm-dd\") %>\n* <%= pkg.description %>\n* <%= pkg.repository.type %>: <%= pkg.repository.url %>\n*/\n(function(angular){\n'use strict';\n", + "footer": "\n}(angular));" + }, + "files": { + "dist/<%= pkg.name %>.min.no-header.js": ["dist/<%= pkg.name %>.no-header.js"] + } + } +} diff --git a/app/bower_components/angular-leaflet-directive/grunt/watch.json b/app/bower_components/angular-leaflet-directive/grunt/watch.json new file mode 100644 index 0000000..ab7de6b --- /dev/null +++ b/app/bower_components/angular-leaflet-directive/grunt/watch.json @@ -0,0 +1,9 @@ +{ + "options": { + "livereload": 7777 + }, + "fast": { + "files": [ "src/**/*.js", "test/unit/**.js", "test/unit/**.coffee", "test/e2e/**.js" ], + "tasks": [ "fast-build" ] + } +} diff --git a/app/bower_components/angular-mocks/.bower.json b/app/bower_components/angular-mocks/.bower.json index d7ca1a0..ee30728 100644 --- a/app/bower_components/angular-mocks/.bower.json +++ b/app/bower_components/angular-mocks/.bower.json @@ -1,18 +1,20 @@ { "name": "angular-mocks", - "version": "1.2.21", + "version": "1.5.6-build.4801+sha.3b5751d", + "license": "MIT", "main": "./angular-mocks.js", + "ignore": [], "dependencies": { - "angular": "1.2.21" + "angular": "1.5.6-build.4801+sha.3b5751d" }, "homepage": "https://github.com/angular/bower-angular-mocks", - "_release": "1.2.21", + "_release": "1.5.6-build.4801+sha.3b5751d", "_resolution": { "type": "version", - "tag": "v1.2.21", - "commit": "ab33e90eaec5bb2c96126e808ae9f44cf59b7205" + "tag": "v1.5.6-build.4801+sha.3b5751d", + "commit": "0a8b4b9b19bdbc42ec5599c329692e8a28084838" }, - "_source": "git://github.com/angular/bower-angular-mocks.git", - "_target": "1.2.21", + "_source": "https://github.com/angular/bower-angular-mocks.git", + "_target": "~1.5.6", "_originalSource": "angular-mocks" } \ No newline at end of file diff --git a/app/bower_components/angular-mocks/README.md b/app/bower_components/angular-mocks/README.md index 3448d28..61b9d8c 100644 --- a/app/bower_components/angular-mocks/README.md +++ b/app/bower_components/angular-mocks/README.md @@ -1,27 +1,48 @@ -# bower-angular-mocks +# packaged angular-mocks -This repo is for distribution on `bower`. The source for this module is in the +This repo is for distribution on `npm` and `bower`. The source for this module is in the [main AngularJS repo](https://github.com/angular/angular.js/tree/master/src/ngMock). Please file issues and pull requests against that repo. ## Install -Install with `bower`: +You can install this package either with `npm` or with `bower`. + +### npm + +```shell +npm install angular-mocks +``` + +You can `require` ngMock modules: + +```js +var angular = require('angular'); +angular.module('myMod', [ + require('angular-animate'), + require('angular-mocks/ngMock'), + require('angular-mocks/ngAnimateMock') +]); +``` + +### bower ```shell bower install angular-mocks ``` +The mocks are then available at `bower_components/angular-mocks/angular-mocks.js`. + ## Documentation Documentation is available on the -[AngularJS docs site](http://docs.angularjs.org/guide/dev_guide.unit-testing). +[AngularJS docs site](https://docs.angularjs.org/guide/unit-testing). ## License The MIT License -Copyright (c) 2010-2012 Google, Inc. http://angularjs.org +Copyright (c) 2010-2015 Google, Inc. http://angularjs.org Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/app/bower_components/angular-mocks/angular-mocks.js b/app/bower_components/angular-mocks/angular-mocks.js index c2f9cec..c136d6a 100644 --- a/app/bower_components/angular-mocks/angular-mocks.js +++ b/app/bower_components/angular-mocks/angular-mocks.js @@ -1,9 +1,9 @@ /** - * @license AngularJS v1.2.21 - * (c) 2010-2014 Google, Inc. http://angularjs.org + * @license AngularJS v1.5.6-build.4801+sha.3b5751d + * (c) 2010-2016 Google, Inc. http://angularjs.org * License: MIT */ -(function(window, angular, undefined) { +(function(window, angular) { 'use strict'; @@ -13,6 +13,7 @@ * @description * * Namespace from 'angular-mocks.js' which contains testing related code. + * */ angular.mock = {}; @@ -24,7 +25,7 @@ angular.mock = {}; * @description * This service is a mock implementation of {@link ng.$browser}. It provides fake * implementation for commonly used browser apis that are hard to test, e.g. setTimeout, xhr, - * cookies, etc... + * cookies, etc. * * The api of this service is the same as that of the real {@link ng.$browser $browser}, except * that there are several helper methods available which can be used in tests. @@ -53,9 +54,10 @@ angular.mock.$Browser = function() { self.onUrlChange = function(listener) { self.pollFns.push( function() { - if (self.$$lastUrl != self.$$url) { + if (self.$$lastUrl !== self.$$url || self.$$state !== self.$$lastState) { self.$$lastUrl = self.$$url; - listener(self.$$url); + self.$$lastState = self.$$state; + listener(self.$$url, self.$$state); } } ); @@ -63,15 +65,16 @@ angular.mock.$Browser = function() { return listener; }; - self.cookieHash = {}; - self.lastCookieHash = {}; + self.$$applicationDestroyed = angular.noop; + self.$$checkUrlChange = angular.noop; + self.deferredFns = []; self.deferredNextId = 0; self.defer = function(fn, delay) { delay = delay || 0; self.deferredFns.push({time:(self.defer.now + delay), fn:fn, id: self.deferredNextId}); - self.deferredFns.sort(function(a,b){ return a.time - b.time;}); + self.deferredFns.sort(function(a, b) { return a.time - b.time;}); return self.deferredNextId++; }; @@ -92,7 +95,7 @@ angular.mock.$Browser = function() { if (fn.id === deferId) fnIndex = index; }); - if (fnIndex !== undefined) { + if (angular.isDefined(fnIndex)) { self.deferredFns.splice(fnIndex, 1); return true; } @@ -114,7 +117,7 @@ angular.mock.$Browser = function() { self.defer.now += delay; } else { if (self.deferredFns.length) { - self.defer.now = self.deferredFns[self.deferredFns.length-1].time; + self.defer.now = self.deferredFns[self.deferredFns.length - 1].time; } else { throw new Error('No deferred tasks to be flushed'); } @@ -125,56 +128,41 @@ angular.mock.$Browser = function() { } }; - self.$$baseHref = ''; + self.$$baseHref = '/'; self.baseHref = function() { return this.$$baseHref; }; }; angular.mock.$Browser.prototype = { -/** - * @name $browser#poll - * - * @description - * run all fns in pollFns - */ + /** + * @name $browser#poll + * + * @description + * run all fns in pollFns + */ poll: function poll() { - angular.forEach(this.pollFns, function(pollFn){ + angular.forEach(this.pollFns, function(pollFn) { pollFn(); }); }, - addPollFn: function(pollFn) { - this.pollFns.push(pollFn); - return pollFn; - }, - - url: function(url, replace) { + url: function(url, replace, state) { + if (angular.isUndefined(state)) { + state = null; + } if (url) { this.$$url = url; + // Native pushState serializes & copies the object; simulate it. + this.$$state = angular.copy(state); return this; } return this.$$url; }, - cookies: function(name, value) { - if (name) { - if (angular.isUndefined(value)) { - delete this.cookieHash[name]; - } else { - if (angular.isString(value) && //strings only - value.length <= 4096) { //strict cookie storage limits - this.cookieHash[name] = value; - } - } - } else { - if (!angular.equals(this.cookieHash, this.lastCookieHash)) { - this.lastCookieHash = angular.copy(this.cookieHash); - this.cookieHash = angular.copy(this.cookieHash); - } - return this.cookieHash; - } + state: function() { + return this.$$state; }, notifyWhenNoOutstandingRequests: function(fn) { @@ -189,7 +177,7 @@ angular.mock.$Browser.prototype = { * * @description * Configures the mock implementation of {@link ng.$exceptionHandler} to rethrow or to log errors - * passed into the `$exceptionHandler`. + * passed to the `$exceptionHandler`. */ /** @@ -198,7 +186,7 @@ angular.mock.$Browser.prototype = { * * @description * Mock implementation of {@link ng.$exceptionHandler} that rethrows or logs errors passed - * into it. See {@link ngMock.$exceptionHandlerProvider $exceptionHandlerProvider} for configuration + * to it. See {@link ngMock.$exceptionHandlerProvider $exceptionHandlerProvider} for configuration * information. * * @@ -238,32 +226,31 @@ angular.mock.$ExceptionHandlerProvider = function() { * * @param {string} mode Mode of operation, defaults to `rethrow`. * - * - `rethrow`: If any errors are passed into the handler in tests, it typically - * means that there is a bug in the application or test, so this mock will - * make these tests fail. * - `log`: Sometimes it is desirable to test that an error is thrown, for this case the `log` * mode stores an array of errors in `$exceptionHandler.errors`, to allow later * assertion of them. See {@link ngMock.$log#assertEmpty assertEmpty()} and * {@link ngMock.$log#reset reset()} + * - `rethrow`: If any errors are passed to the handler in tests, it typically means that there + * is a bug in the application or test, so this mock will make these tests fail. + * For any implementations that expect exceptions to be thrown, the `rethrow` mode + * will also maintain a log of thrown errors. */ this.mode = function(mode) { - switch(mode) { - case 'rethrow': - handler = function(e) { - throw e; - }; - break; - case 'log': - var errors = []; + switch (mode) { + case 'log': + case 'rethrow': + var errors = []; handler = function(e) { if (arguments.length == 1) { errors.push(e); } else { errors.push([].slice.call(arguments, 0)); } + if (mode === "rethrow") { + throw e; + } }; - handler.errors = errors; break; default: @@ -305,7 +292,7 @@ angular.mock.$LogProvider = function() { } }; - this.$get = function () { + this.$get = function() { var $log = { log: function() { $log.log.logs.push(concat([], arguments, 0)); }, warn: function() { $log.warn.logs.push(concat([], arguments, 0)); }, @@ -325,13 +312,13 @@ angular.mock.$LogProvider = function() { * @description * Reset all of the logging arrays to empty. */ - $log.reset = function () { + $log.reset = function() { /** * @ngdoc property * @name $log#log.logs * * @description - * Array of messages logged using {@link ngMock.$log#log}. + * Array of messages logged using {@link ng.$log#log `log()`}. * * @example * ```js @@ -345,7 +332,7 @@ angular.mock.$LogProvider = function() { * @name $log#info.logs * * @description - * Array of messages logged using {@link ngMock.$log#info}. + * Array of messages logged using {@link ng.$log#info `info()`}. * * @example * ```js @@ -359,7 +346,7 @@ angular.mock.$LogProvider = function() { * @name $log#warn.logs * * @description - * Array of messages logged using {@link ngMock.$log#warn}. + * Array of messages logged using {@link ng.$log#warn `warn()`}. * * @example * ```js @@ -373,7 +360,7 @@ angular.mock.$LogProvider = function() { * @name $log#error.logs * * @description - * Array of messages logged using {@link ngMock.$log#error}. + * Array of messages logged using {@link ng.$log#error `error()`}. * * @example * ```js @@ -387,7 +374,7 @@ angular.mock.$LogProvider = function() { * @name $log#debug.logs * * @description - * Array of messages logged using {@link ngMock.$log#debug}. + * Array of messages logged using {@link ng.$log#debug `debug()`}. * * @example * ```js @@ -403,21 +390,21 @@ angular.mock.$LogProvider = function() { * @name $log#assertEmpty * * @description - * Assert that the all of the logging methods have no logged messages. If messages present, an - * exception is thrown. + * Assert that all of the logging methods have no logged messages. If any messages are present, + * an exception is thrown. */ $log.assertEmpty = function() { var errors = []; angular.forEach(['error', 'warn', 'info', 'log', 'debug'], function(logLevel) { angular.forEach($log[logLevel].logs, function(log) { - angular.forEach(log, function (logItem) { + angular.forEach(log, function(logItem) { errors.push('MOCK $log (' + logLevel + '): ' + String(logItem) + '\n' + (logItem.stack || '')); }); }); }); if (errors.length) { - errors.unshift("Expected $log to be empty! Either a message was logged unexpectedly, or "+ + errors.unshift("Expected $log to be empty! Either a message was logged unexpectedly, or " + "an expected log message was not checked and removed:"); errors.push(''); throw new Error(errors.join('\n---------\n')); @@ -447,23 +434,28 @@ angular.mock.$LogProvider = function() { * indefinitely. * @param {boolean=} [invokeApply=true] If set to `false` skips model dirty checking, otherwise * will invoke `fn` within the {@link ng.$rootScope.Scope#$apply $apply} block. + * @param {...*=} Pass additional parameters to the executed function. * @returns {promise} A promise which will be notified on each iteration. */ angular.mock.$IntervalProvider = function() { - this.$get = ['$rootScope', '$q', - function($rootScope, $q) { + this.$get = ['$browser', '$rootScope', '$q', '$$q', + function($browser, $rootScope, $q, $$q) { var repeatFns = [], nextRepeatId = 0, now = 0; var $interval = function(fn, delay, count, invokeApply) { - var deferred = $q.defer(), - promise = deferred.promise, + var hasParams = arguments.length > 4, + args = hasParams ? Array.prototype.slice.call(arguments, 4) : [], iteration = 0, - skipApply = (angular.isDefined(invokeApply) && !invokeApply); + skipApply = (angular.isDefined(invokeApply) && !invokeApply), + deferred = (skipApply ? $$q : $q).defer(), + promise = deferred.promise; count = (angular.isDefined(count)) ? count : 0; - promise.then(null, null, fn); + promise.then(null, function() {}, (!hasParams) ? fn : function() { + fn.apply(null, args); + }); promise.$$intervalId = nextRepeatId; @@ -478,12 +470,16 @@ angular.mock.$IntervalProvider = function() { if (fn.id === promise.$$intervalId) fnIndex = index; }); - if (fnIndex !== undefined) { + if (angular.isDefined(fnIndex)) { repeatFns.splice(fnIndex, 1); } } - if (!skipApply) $rootScope.$apply(); + if (skipApply) { + $browser.defer.flush(); + } else { + $rootScope.$apply(); + } } repeatFns.push({ @@ -493,7 +489,7 @@ angular.mock.$IntervalProvider = function() { id: nextRepeatId, deferred: deferred }); - repeatFns.sort(function(a,b){ return a.nextTime - b.nextTime;}); + repeatFns.sort(function(a, b) { return a.nextTime - b.nextTime;}); nextRepeatId++; return promise; @@ -509,14 +505,15 @@ angular.mock.$IntervalProvider = function() { * @returns {boolean} Returns `true` if the task was successfully cancelled. */ $interval.cancel = function(promise) { - if(!promise) return false; + if (!promise) return false; var fnIndex; angular.forEach(repeatFns, function(fn, index) { if (fn.id === promise.$$intervalId) fnIndex = index; }); - if (fnIndex !== undefined) { + if (angular.isDefined(fnIndex)) { + repeatFns[fnIndex].deferred.promise.then(undefined, function() {}); repeatFns[fnIndex].deferred.reject('canceled'); repeatFns.splice(fnIndex, 1); return true; @@ -542,7 +539,7 @@ angular.mock.$IntervalProvider = function() { var task = repeatFns[0]; task.fn(); task.nextTime += task.delay; - repeatFns.sort(function(a,b){ return a.nextTime - b.nextTime;}); + repeatFns.sort(function(a, b) { return a.nextTime - b.nextTime;}); } return millis; }; @@ -557,7 +554,7 @@ angular.mock.$IntervalProvider = function() { * This directive should go inside the anonymous function but a bug in JSHint means that it would * not be enacted early enough to prevent the warning. */ -var R_ISO8061_STR = /^(\d{4})-?(\d\d)-?(\d\d)(?:T(\d\d)(?:\:?(\d\d)(?:\:?(\d\d)(?:\.(\d{3}))?)?)?(Z|([+-])(\d\d):?(\d\d)))?$/; +var R_ISO8061_STR = /^(-?\d{4})-?(\d\d)-?(\d\d)(?:T(\d\d)(?:\:?(\d\d)(?:\:?(\d\d)(?:\.(\d{3}))?)?)?(Z|([+-])(\d\d):?(\d\d)))?$/; function jsonStringToDate(string) { var match; @@ -566,33 +563,34 @@ function jsonStringToDate(string) { tzHour = 0, tzMin = 0; if (match[9]) { - tzHour = int(match[9] + match[10]); - tzMin = int(match[9] + match[11]); + tzHour = toInt(match[9] + match[10]); + tzMin = toInt(match[9] + match[11]); } - date.setUTCFullYear(int(match[1]), int(match[2]) - 1, int(match[3])); - date.setUTCHours(int(match[4]||0) - tzHour, - int(match[5]||0) - tzMin, - int(match[6]||0), - int(match[7]||0)); + date.setUTCFullYear(toInt(match[1]), toInt(match[2]) - 1, toInt(match[3])); + date.setUTCHours(toInt(match[4] || 0) - tzHour, + toInt(match[5] || 0) - tzMin, + toInt(match[6] || 0), + toInt(match[7] || 0)); return date; } return string; } -function int(str) { +function toInt(str) { return parseInt(str, 10); } -function padNumber(num, digits, trim) { +function padNumberInMock(num, digits, trim) { var neg = ''; if (num < 0) { neg = '-'; num = -num; } num = '' + num; - while(num.length < digits) num = '0' + num; - if (trim) + while (num.length < digits) num = '0' + num; + if (trim) { num = num.substr(num.length - digits); + } return neg + num; } @@ -634,7 +632,7 @@ function padNumber(num, digits, trim) { * ``` * */ -angular.mock.TzDate = function (offset, timestamp) { +angular.mock.TzDate = function(offset, timestamp) { var self = new Date(0); if (angular.isString(timestamp)) { var tsStr = timestamp; @@ -642,17 +640,18 @@ angular.mock.TzDate = function (offset, timestamp) { self.origDate = jsonStringToDate(timestamp); timestamp = self.origDate.getTime(); - if (isNaN(timestamp)) + if (isNaN(timestamp)) { throw { name: "Illegal Argument", message: "Arg '" + tsStr + "' passed into TzDate constructor is not a valid date string" }; + } } else { self.origDate = new Date(timestamp); } var localOffset = new Date(timestamp).getTimezoneOffset(); - self.offsetDiff = localOffset*60*1000 - offset*1000*60*60; + self.offsetDiff = localOffset * 60 * 1000 - offset * 1000 * 60 * 60; self.date = new Date(timestamp + self.offsetDiff); self.getTime = function() { @@ -730,13 +729,13 @@ angular.mock.TzDate = function (offset, timestamp) { // provide this method only on browsers that already have it if (self.toISOString) { self.toISOString = function() { - return padNumber(self.origDate.getUTCFullYear(), 4) + '-' + - padNumber(self.origDate.getUTCMonth() + 1, 2) + '-' + - padNumber(self.origDate.getUTCDate(), 2) + 'T' + - padNumber(self.origDate.getUTCHours(), 2) + ':' + - padNumber(self.origDate.getUTCMinutes(), 2) + ':' + - padNumber(self.origDate.getUTCSeconds(), 2) + '.' + - padNumber(self.origDate.getUTCMilliseconds(), 3) + 'Z'; + return padNumberInMock(self.origDate.getUTCFullYear(), 4) + '-' + + padNumberInMock(self.origDate.getUTCMonth() + 1, 2) + '-' + + padNumberInMock(self.origDate.getUTCDate(), 2) + 'T' + + padNumberInMock(self.origDate.getUTCHours(), 2) + ':' + + padNumberInMock(self.origDate.getUTCMinutes(), 2) + ':' + + padNumberInMock(self.origDate.getUTCSeconds(), 2) + '.' + + padNumberInMock(self.origDate.getUTCMilliseconds(), 3) + 'Z'; }; } @@ -761,48 +760,170 @@ angular.mock.TzDate = function (offset, timestamp) { angular.mock.TzDate.prototype = Date.prototype; /* jshint +W101 */ + +/** + * @ngdoc service + * @name $animate + * + * @description + * Mock implementation of the {@link ng.$animate `$animate`} service. Exposes two additional methods + * for testing animations. + */ angular.mock.animate = angular.module('ngAnimateMock', ['ng']) .config(['$provide', function($provide) { - var reflowQueue = []; - $provide.value('$$animateReflow', function(fn) { - var index = reflowQueue.length; - reflowQueue.push(fn); - return function cancel() { - reflowQueue.splice(index, 1); - }; + $provide.factory('$$forceReflow', function() { + function reflowFn() { + reflowFn.totalReflows++; + } + reflowFn.totalReflows = 0; + return reflowFn; }); - $provide.decorator('$animate', function($delegate, $$asyncCallback) { + $provide.factory('$$animateAsyncRun', function() { + var queue = []; + var queueFn = function() { + return function(fn) { + queue.push(fn); + }; + }; + queueFn.flush = function() { + if (queue.length === 0) return false; + + for (var i = 0; i < queue.length; i++) { + queue[i](); + } + queue = []; + + return true; + }; + return queueFn; + }); + + $provide.decorator('$$animateJs', ['$delegate', function($delegate) { + var runners = []; + + var animateJsConstructor = function() { + var animator = $delegate.apply($delegate, arguments); + // If no javascript animation is found, animator is undefined + if (animator) { + runners.push(animator); + } + return animator; + }; + + animateJsConstructor.$closeAndFlush = function() { + runners.forEach(function(runner) { + runner.end(); + }); + runners = []; + }; + + return animateJsConstructor; + }]); + + $provide.decorator('$animateCss', ['$delegate', function($delegate) { + var runners = []; + + var animateCssConstructor = function(element, options) { + var animator = $delegate(element, options); + runners.push(animator); + return animator; + }; + + animateCssConstructor.$closeAndFlush = function() { + runners.forEach(function(runner) { + runner.end(); + }); + runners = []; + }; + + return animateCssConstructor; + }]); + + $provide.decorator('$animate', ['$delegate', '$timeout', '$browser', '$$rAF', '$animateCss', '$$animateJs', + '$$forceReflow', '$$animateAsyncRun', '$rootScope', + function($delegate, $timeout, $browser, $$rAF, $animateCss, $$animateJs, + $$forceReflow, $$animateAsyncRun, $rootScope) { var animate = { - queue : [], - enabled : $delegate.enabled, - triggerCallbacks : function() { - $$asyncCallback.flush(); + queue: [], + cancel: $delegate.cancel, + on: $delegate.on, + off: $delegate.off, + pin: $delegate.pin, + get reflows() { + return $$forceReflow.totalReflows; }, - triggerReflow : function() { - angular.forEach(reflowQueue, function(fn) { - fn(); - }); - reflowQueue = []; + enabled: $delegate.enabled, + /** + * @ngdoc method + * @name $animate#closeAndFlush + * @description + * + * This method will close all pending animations (both {@link ngAnimate#javascript-based-animations Javascript} + * and {@link ngAnimate.$animateCss CSS}) and it will also flush any remaining animation frames and/or callbacks. + */ + closeAndFlush: function() { + // we allow the flush command to swallow the errors + // because depending on whether CSS or JS animations are + // used, there may not be a RAF flush. The primary flush + // at the end of this function must throw an exception + // because it will track if there were pending animations + this.flush(true); + $animateCss.$closeAndFlush(); + $$animateJs.$closeAndFlush(); + this.flush(); + }, + /** + * @ngdoc method + * @name $animate#flush + * @description + * + * This method is used to flush the pending callbacks and animation frames to either start + * an animation or conclude an animation. Note that this will not actually close an + * actively running animation (see {@link ngMock.$animate#closeAndFlush `closeAndFlush()`} for that). + */ + flush: function(hideErrors) { + $rootScope.$digest(); + + var doNextRun, somethingFlushed = false; + do { + doNextRun = false; + + if ($$rAF.queue.length) { + $$rAF.flush(); + doNextRun = somethingFlushed = true; + } + + if ($$animateAsyncRun.flush()) { + doNextRun = somethingFlushed = true; + } + } while (doNextRun); + + if (!somethingFlushed && !hideErrors) { + throw new Error('No pending animations ready to be closed or flushed'); + } + + $rootScope.$digest(); } }; angular.forEach( - ['enter','leave','move','addClass','removeClass','setClass'], function(method) { + ['animate','enter','leave','move','addClass','removeClass','setClass'], function(method) { animate[method] = function() { animate.queue.push({ - event : method, - element : arguments[0], - args : arguments + event: method, + element: arguments[0], + options: arguments[arguments.length - 1], + args: arguments }); - $delegate[method].apply($delegate, arguments); + return $delegate[method].apply($delegate, arguments); }; }); return animate; - }); + }]); }]); @@ -862,13 +983,13 @@ angular.mock.dump = function(object) { function serializeScope(scope, offset) { offset = offset || ' '; var log = [offset + 'Scope(' + scope.$id + '): {']; - for ( var key in scope ) { + for (var key in scope) { if (Object.prototype.hasOwnProperty.call(scope, key) && !key.match(/^(\$|this)/)) { log.push(' ' + key + ': ' + angular.toJson(scope[key])); } } var child = scope.$$childHead; - while(child) { + while (child) { log.push(serializeScope(child, offset + ' ')); child = child.$$nextSibling; } @@ -884,8 +1005,10 @@ angular.mock.dump = function(object) { * Fake HTTP backend implementation suitable for unit testing applications that use the * {@link ng.$http $http service}. * - * *Note*: For fake HTTP backend implementation suitable for end-to-end testing or backend-less + *
        + * **Note**: For fake HTTP backend implementation suitable for end-to-end testing or backend-less * development please see {@link ngMockE2E.$httpBackend e2e $httpBackend mock}. + *
        * * During unit testing, we want our unit tests to run quickly and have no external dependencies so * we don’t want to send [XHR](https://developer.mozilla.org/en/xmlhttprequest) or @@ -909,7 +1032,7 @@ angular.mock.dump = function(object) { * - `$httpBackend.when` - specifies a backend definition * * - * # Request Expectations vs Backend Definitions + * ## Request Expectations vs Backend Definitions * * Request expectations provide a way to make assertions about requests made by the application and * to define responses for those requests. The test will fail if the expected requests are not made @@ -965,7 +1088,7 @@ angular.mock.dump = function(object) { * the request. The response from the first matched definition is returned. * * - * # Flushing HTTP requests + * ## Flushing HTTP requests * * The $httpBackend used in production always responds to requests asynchronously. If we preserved * this behavior in unit testing, we'd have to create async unit tests, which are hard to write, @@ -975,28 +1098,33 @@ angular.mock.dump = function(object) { * the async api of the backend, while allowing the test to execute synchronously. * * - * # Unit testing with mock $httpBackend + * ## Unit testing with mock $httpBackend * The following code shows how to setup and use the mock backend when unit testing a controller. * First we create the controller under test: * ```js + // The module code + angular + .module('MyApp', []) + .controller('MyController', MyController); + // The controller code function MyController($scope, $http) { var authToken; - $http.get('/auth.py').success(function(data, status, headers) { - authToken = headers('A-Token'); - $scope.user = data; + $http.get('/auth.py').then(function(response) { + authToken = response.headers('A-Token'); + $scope.user = response.data; }); $scope.saveMessage = function(message) { var headers = { 'Authorization': authToken }; $scope.status = 'Saving...'; - $http.post('/add-msg.py', message, { headers: headers } ).success(function(response) { + $http.post('/add-msg.py', message, { headers: headers } ).then(function(response) { $scope.status = ''; - }).error(function() { - $scope.status = 'ERROR!'; + }).catch(function() { + $scope.status = 'Failed...'; }); }; } @@ -1007,13 +1135,17 @@ angular.mock.dump = function(object) { ```js // testing controller describe('MyController', function() { - var $httpBackend, $rootScope, createController; + var $httpBackend, $rootScope, createController, authRequestHandler; + + // Set up the module + beforeEach(module('MyApp')); beforeEach(inject(function($injector) { // Set up the mock http service responses $httpBackend = $injector.get('$httpBackend'); // backend definition common for all tests - $httpBackend.when('GET', '/auth.py').respond({userId: 'userX'}, {'A-Token': 'xxx'}); + authRequestHandler = $httpBackend.when('GET', '/auth.py') + .respond({userId: 'userX'}, {'A-Token': 'xxx'}); // Get hold of a scope (i.e. the root scope) $rootScope = $injector.get('$rootScope'); @@ -1039,6 +1171,18 @@ angular.mock.dump = function(object) { }); + it('should fail authentication', function() { + + // Notice how you can change the response even after it was set + authRequestHandler.respond(401, ''); + + $httpBackend.expectGET('/auth.py'); + var controller = createController(); + $httpBackend.flush(); + expect($rootScope.status).toBe('Failed...'); + }); + + it('should send msg to server', function() { var controller = createController(); $httpBackend.flush(); @@ -1061,7 +1205,7 @@ angular.mock.dump = function(object) { $httpBackend.flush(); $httpBackend.expectPOST('/add-msg.py', undefined, function(headers) { - // check if the header was send, if it wasn't the expectation won't + // check if the header was sent, if it wasn't the expectation won't // match the request and the test will fail return headers['Authorization'] == 'xxx'; }).respond(201, ''); @@ -1070,10 +1214,90 @@ angular.mock.dump = function(object) { $httpBackend.flush(); }); }); - ``` + ``` + * + * ## Dynamic responses + * + * You define a response to a request by chaining a call to `respond()` onto a definition or expectation. + * If you provide a **callback** as the first parameter to `respond(callback)` then you can dynamically generate + * a response based on the properties of the request. + * + * The `callback` function should be of the form `function(method, url, data, headers, params)`. + * + * ### Query parameters + * + * By default, query parameters on request URLs are parsed into the `params` object. So a request URL + * of `/list?q=searchstr&orderby=-name` would set `params` to be `{q: 'searchstr', orderby: '-name'}`. + * + * ### Regex parameter matching + * + * If an expectation or definition uses a **regex** to match the URL, you can provide an array of **keys** via a + * `params` argument. The index of each **key** in the array will match the index of a **group** in the + * **regex**. + * + * The `params` object in the **callback** will now have properties with these keys, which hold the value of the + * corresponding **group** in the **regex**. + * + * This also applies to the `when` and `expect` shortcut methods. + * + * + * ```js + * $httpBackend.expect('GET', /\/user\/(.+)/, undefined, undefined, ['id']) + * .respond(function(method, url, data, headers, params) { + * // for requested url of '/user/1234' params is {id: '1234'} + * }); + * + * $httpBackend.whenPATCH(/\/user\/(.+)\/article\/(.+)/, undefined, undefined, ['user', 'article']) + * .respond(function(method, url, data, headers, params) { + * // for url of '/user/1234/article/567' params is {user: '1234', article: '567'} + * }); + * ``` + * + * ## Matching route requests + * + * For extra convenience, `whenRoute` and `expectRoute` shortcuts are available. These methods offer colon + * delimited matching of the url path, ignoring the query string. This allows declarations + * similar to how application routes are configured with `$routeProvider`. Because these methods convert + * the definition url to regex, declaration order is important. Combined with query parameter parsing, + * the following is possible: + * + ```js + $httpBackend.whenRoute('GET', '/users/:id') + .respond(function(method, url, data, headers, params) { + return [200, MockUserList[Number(params.id)]]; + }); + + $httpBackend.whenRoute('GET', '/users') + .respond(function(method, url, data, headers, params) { + var userList = angular.copy(MockUserList), + defaultSort = 'lastName', + count, pages, isPrevious, isNext; + + // paged api response '/v1/users?page=2' + params.page = Number(params.page) || 1; + + // query for last names '/v1/users?q=Archer' + if (params.q) { + userList = $filter('filter')({lastName: params.q}); + } + + pages = Math.ceil(userList.length / pagingLength); + isPrevious = params.page > 1; + isNext = params.page < pages; + + return [200, { + count: userList.length, + previous: isPrevious, + next: isNext, + // sort field -> '/v1/users?sortBy=firstName' + results: $filter('orderBy')(userList, params.sortBy || defaultSort) + .splice((params.page - 1) * pagingLength, pagingLength) + }]; + }); + ``` */ angular.mock.$HttpBackendProvider = function() { - this.$get = ['$rootScope', createHttpBackendMock]; + this.$get = ['$rootScope', '$timeout', createHttpBackendMock]; }; /** @@ -1090,7 +1314,7 @@ angular.mock.$HttpBackendProvider = function() { * @param {Object=} $browser Auto-flushing enabled if specified * @return {Object} Instance of $httpBackend mock */ -function createHttpBackendMock($rootScope, $delegate, $browser) { +function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) { var definitions = [], expectations = [], responses = [], @@ -1103,16 +1327,20 @@ function createHttpBackendMock($rootScope, $delegate, $browser) { return function() { return angular.isNumber(status) ? [status, data, headers, statusText] - : [200, status, data]; + : [200, status, data, headers]; }; } // TODO(vojta): change params to: method, url, data, headers, callback - function $httpBackend(method, url, data, callback, headers, timeout, withCredentials) { + function $httpBackend(method, url, data, callback, headers, timeout, withCredentials, responseType, eventHandlers, uploadEventHandlers) { + var xhr = new MockXhr(), expectation = expectations[0], wasExpected = false; + xhr.$$events = eventHandlers; + xhr.upload.$$events = uploadEventHandlers; + function prettyPrint(data) { return (angular.isString(data) || angular.isFunction(data) || data instanceof RegExp) ? data @@ -1120,12 +1348,14 @@ function createHttpBackendMock($rootScope, $delegate, $browser) { } function wrapResponse(wrapped) { - if (!$browser && timeout && timeout.then) timeout.then(handleTimeout); + if (!$browser && timeout) { + timeout.then ? timeout.then(handleTimeout) : $timeout(handleTimeout, timeout); + } return handleResponse; function handleResponse() { - var response = wrapped.response(method, url, data, headers); + var response = wrapped.response(method, url, data, headers, wrapped.params(url)); xhr.$$respHeaders = response[2]; callback(copy(response[0]), copy(response[1]), xhr.getAllResponseHeaders(), copy(response[3] || '')); @@ -1143,14 +1373,16 @@ function createHttpBackendMock($rootScope, $delegate, $browser) { } if (expectation && expectation.match(method, url)) { - if (!expectation.matchData(data)) + if (!expectation.matchData(data)) { throw new Error('Expected ' + expectation + ' with different data\n' + 'EXPECTED: ' + prettyPrint(expectation.data) + '\nGOT: ' + data); + } - if (!expectation.matchHeaders(headers)) + if (!expectation.matchHeaders(headers)) { throw new Error('Expected ' + expectation + ' with different headers\n' + 'EXPECTED: ' + prettyPrint(expectation.headers) + '\nGOT: ' + prettyPrint(headers)); + } expectations.shift(); @@ -1168,7 +1400,7 @@ function createHttpBackendMock($rootScope, $delegate, $browser) { // if $browser specified, we do auto flush all requests ($browser ? $browser.defer : responsesPush)(wrapResponse(definition)); } else if (definition.passThrough) { - $delegate(method, url, data, callback, headers, timeout, withCredentials); + $delegate(method, url, data, callback, headers, timeout, withCredentials, responseType, eventHandlers, uploadEventHandlers); } else throw new Error('No response defined !'); return; } @@ -1186,32 +1418,42 @@ function createHttpBackendMock($rootScope, $delegate, $browser) { * Creates a new backend definition. * * @param {string} method HTTP method. - * @param {string|RegExp} url HTTP url. + * @param {string|RegExp|function(string)} url HTTP url or function that receives a url + * and returns true if the url matches the current definition. * @param {(string|RegExp|function(string))=} data HTTP request body or function that receives * data string and returns true if the data is as expected. * @param {(Object|function(Object))=} headers HTTP headers or function that receives http header * object and returns true if the headers match the current definition. + * @param {(Array)=} keys Array of keys to assign to regex matches in request url described above. * @returns {requestHandler} Returns an object with `respond` method that controls how a matched - * request is handled. + * request is handled. You can save this object for later use and invoke `respond` again in + * order to change how a matched request is handled. * * - respond – - * `{function([status,] data[, headers, statusText]) - * | function(function(method, url, data, headers)}` + * ```js + * {function([status,] data[, headers, statusText]) + * | function(function(method, url, data, headers, params)} + * ``` * – The respond method takes a set of static data to be returned or a function that can - * return an array containing response status (number), response data (string), response - * headers (Object), and the text for the status (string). + * return an array containing response status (number), response data (Array|Object|string), + * response headers (Object), and the text for the status (string). The respond method returns + * the `requestHandler` object for possible overrides. */ - $httpBackend.when = function(method, url, data, headers) { - var definition = new MockHttpExpectation(method, url, data, headers), + $httpBackend.when = function(method, url, data, headers, keys) { + var definition = new MockHttpExpectation(method, url, data, headers, keys), chain = { respond: function(status, data, headers, statusText) { + definition.passThrough = undefined; definition.response = createResponse(status, data, headers, statusText); + return chain; } }; if ($browser) { chain.passThrough = function() { + definition.response = undefined; definition.passThrough = true; + return chain; }; } @@ -1225,10 +1467,13 @@ function createHttpBackendMock($rootScope, $delegate, $browser) { * @description * Creates a new backend definition for GET requests. For more info see `when()`. * - * @param {string|RegExp} url HTTP url. + * @param {string|RegExp|function(string)} url HTTP url or function that receives a url + * and returns true if the url matches the current definition. * @param {(Object|function(Object))=} headers HTTP headers. - * @returns {requestHandler} Returns an object with `respond` method that control how a matched - * request is handled. + * @param {(Array)=} keys Array of keys to assign to regex matches in request url described above. + * @returns {requestHandler} Returns an object with `respond` method that controls how a matched + * request is handled. You can save this object for later use and invoke `respond` again in + * order to change how a matched request is handled. */ /** @@ -1237,10 +1482,13 @@ function createHttpBackendMock($rootScope, $delegate, $browser) { * @description * Creates a new backend definition for HEAD requests. For more info see `when()`. * - * @param {string|RegExp} url HTTP url. + * @param {string|RegExp|function(string)} url HTTP url or function that receives a url + * and returns true if the url matches the current definition. * @param {(Object|function(Object))=} headers HTTP headers. - * @returns {requestHandler} Returns an object with `respond` method that control how a matched - * request is handled. + * @param {(Array)=} keys Array of keys to assign to regex matches in request url described above. + * @returns {requestHandler} Returns an object with `respond` method that controls how a matched + * request is handled. You can save this object for later use and invoke `respond` again in + * order to change how a matched request is handled. */ /** @@ -1249,10 +1497,13 @@ function createHttpBackendMock($rootScope, $delegate, $browser) { * @description * Creates a new backend definition for DELETE requests. For more info see `when()`. * - * @param {string|RegExp} url HTTP url. + * @param {string|RegExp|function(string)} url HTTP url or function that receives a url + * and returns true if the url matches the current definition. * @param {(Object|function(Object))=} headers HTTP headers. - * @returns {requestHandler} Returns an object with `respond` method that control how a matched - * request is handled. + * @param {(Array)=} keys Array of keys to assign to regex matches in request url described above. + * @returns {requestHandler} Returns an object with `respond` method that controls how a matched + * request is handled. You can save this object for later use and invoke `respond` again in + * order to change how a matched request is handled. */ /** @@ -1261,12 +1512,15 @@ function createHttpBackendMock($rootScope, $delegate, $browser) { * @description * Creates a new backend definition for POST requests. For more info see `when()`. * - * @param {string|RegExp} url HTTP url. + * @param {string|RegExp|function(string)} url HTTP url or function that receives a url + * and returns true if the url matches the current definition. * @param {(string|RegExp|function(string))=} data HTTP request body or function that receives * data string and returns true if the data is as expected. * @param {(Object|function(Object))=} headers HTTP headers. - * @returns {requestHandler} Returns an object with `respond` method that control how a matched - * request is handled. + * @param {(Array)=} keys Array of keys to assign to regex matches in request url described above. + * @returns {requestHandler} Returns an object with `respond` method that controls how a matched + * request is handled. You can save this object for later use and invoke `respond` again in + * order to change how a matched request is handled. */ /** @@ -1275,12 +1529,15 @@ function createHttpBackendMock($rootScope, $delegate, $browser) { * @description * Creates a new backend definition for PUT requests. For more info see `when()`. * - * @param {string|RegExp} url HTTP url. + * @param {string|RegExp|function(string)} url HTTP url or function that receives a url + * and returns true if the url matches the current definition. * @param {(string|RegExp|function(string))=} data HTTP request body or function that receives * data string and returns true if the data is as expected. * @param {(Object|function(Object))=} headers HTTP headers. - * @returns {requestHandler} Returns an object with `respond` method that control how a matched - * request is handled. + * @param {(Array)=} keys Array of keys to assign to regex matches in request url described above. + * @returns {requestHandler} Returns an object with `respond` method that controls how a matched + * request is handled. You can save this object for later use and invoke `respond` again in + * order to change how a matched request is handled. */ /** @@ -1289,12 +1546,61 @@ function createHttpBackendMock($rootScope, $delegate, $browser) { * @description * Creates a new backend definition for JSONP requests. For more info see `when()`. * - * @param {string|RegExp} url HTTP url. - * @returns {requestHandler} Returns an object with `respond` method that control how a matched - * request is handled. + * @param {string|RegExp|function(string)} url HTTP url or function that receives a url + * and returns true if the url matches the current definition. + * @param {(Array)=} keys Array of keys to assign to regex matches in request url described above. + * @returns {requestHandler} Returns an object with `respond` method that controls how a matched + * request is handled. You can save this object for later use and invoke `respond` again in + * order to change how a matched request is handled. */ createShortMethods('when'); + /** + * @ngdoc method + * @name $httpBackend#whenRoute + * @description + * Creates a new backend definition that compares only with the requested route. + * + * @param {string} method HTTP method. + * @param {string} url HTTP url string that supports colon param matching. + * @returns {requestHandler} Returns an object with `respond` method that controls how a matched + * request is handled. You can save this object for later use and invoke `respond` again in + * order to change how a matched request is handled. See #when for more info. + */ + $httpBackend.whenRoute = function(method, url) { + var pathObj = parseRoute(url); + return $httpBackend.when(method, pathObj.regexp, undefined, undefined, pathObj.keys); + }; + + function parseRoute(url) { + var ret = { + regexp: url + }, + keys = ret.keys = []; + + if (!url || !angular.isString(url)) return ret; + + url = url + .replace(/([().])/g, '\\$1') + .replace(/(\/)?:(\w+)([\?\*])?/g, function(_, slash, key, option) { + var optional = option === '?' ? option : null; + var star = option === '*' ? option : null; + keys.push({ name: key, optional: !!optional }); + slash = slash || ''; + return '' + + (optional ? '' : slash) + + '(?:' + + (optional ? slash : '') + + (star && '(.+?)' || '([^/]+)') + + (optional || '') + + ')' + + (optional || ''); + }) + .replace(/([\/$\*])/g, '\\$1'); + + ret.regexp = new RegExp('^' + url, 'i'); + return ret; + } /** * @ngdoc method @@ -1303,32 +1609,40 @@ function createHttpBackendMock($rootScope, $delegate, $browser) { * Creates a new request expectation. * * @param {string} method HTTP method. - * @param {string|RegExp} url HTTP url. + * @param {string|RegExp|function(string)} url HTTP url or function that receives a url + * and returns true if the url matches the current definition. * @param {(string|RegExp|function(string)|Object)=} data HTTP request body or function that * receives data string and returns true if the data is as expected, or Object if request body * is in JSON format. * @param {(Object|function(Object))=} headers HTTP headers or function that receives http header * object and returns true if the headers match the current expectation. - * @returns {requestHandler} Returns an object with `respond` method that control how a matched - * request is handled. + * @param {(Array)=} keys Array of keys to assign to regex matches in request url described above. + * @returns {requestHandler} Returns an object with `respond` method that controls how a matched + * request is handled. You can save this object for later use and invoke `respond` again in + * order to change how a matched request is handled. * * - respond – - * `{function([status,] data[, headers, statusText]) - * | function(function(method, url, data, headers)}` + * ``` + * { function([status,] data[, headers, statusText]) + * | function(function(method, url, data, headers, params)} + * ``` * – The respond method takes a set of static data to be returned or a function that can - * return an array containing response status (number), response data (string), response - * headers (Object), and the text for the status (string). + * return an array containing response status (number), response data (Array|Object|string), + * response headers (Object), and the text for the status (string). The respond method returns + * the `requestHandler` object for possible overrides. */ - $httpBackend.expect = function(method, url, data, headers) { - var expectation = new MockHttpExpectation(method, url, data, headers); - expectations.push(expectation); - return { - respond: function (status, data, headers, statusText) { - expectation.response = createResponse(status, data, headers, statusText); - } - }; - }; + $httpBackend.expect = function(method, url, data, headers, keys) { + var expectation = new MockHttpExpectation(method, url, data, headers, keys), + chain = { + respond: function(status, data, headers, statusText) { + expectation.response = createResponse(status, data, headers, statusText); + return chain; + } + }; + expectations.push(expectation); + return chain; + }; /** * @ngdoc method @@ -1336,10 +1650,13 @@ function createHttpBackendMock($rootScope, $delegate, $browser) { * @description * Creates a new request expectation for GET requests. For more info see `expect()`. * - * @param {string|RegExp} url HTTP url. + * @param {string|RegExp|function(string)} url HTTP url or function that receives a url + * and returns true if the url matches the current definition. * @param {Object=} headers HTTP headers. - * @returns {requestHandler} Returns an object with `respond` method that control how a matched - * request is handled. See #expect for more info. + * @param {(Array)=} keys Array of keys to assign to regex matches in request url described above. + * @returns {requestHandler} Returns an object with `respond` method that controls how a matched + * request is handled. You can save this object for later use and invoke `respond` again in + * order to change how a matched request is handled. See #expect for more info. */ /** @@ -1348,10 +1665,13 @@ function createHttpBackendMock($rootScope, $delegate, $browser) { * @description * Creates a new request expectation for HEAD requests. For more info see `expect()`. * - * @param {string|RegExp} url HTTP url. + * @param {string|RegExp|function(string)} url HTTP url or function that receives a url + * and returns true if the url matches the current definition. * @param {Object=} headers HTTP headers. - * @returns {requestHandler} Returns an object with `respond` method that control how a matched - * request is handled. + * @param {(Array)=} keys Array of keys to assign to regex matches in request url described above. + * @returns {requestHandler} Returns an object with `respond` method that controls how a matched + * request is handled. You can save this object for later use and invoke `respond` again in + * order to change how a matched request is handled. */ /** @@ -1360,10 +1680,13 @@ function createHttpBackendMock($rootScope, $delegate, $browser) { * @description * Creates a new request expectation for DELETE requests. For more info see `expect()`. * - * @param {string|RegExp} url HTTP url. + * @param {string|RegExp|function(string)} url HTTP url or function that receives a url + * and returns true if the url matches the current definition. * @param {Object=} headers HTTP headers. - * @returns {requestHandler} Returns an object with `respond` method that control how a matched - * request is handled. + * @param {(Array)=} keys Array of keys to assign to regex matches in request url described above. + * @returns {requestHandler} Returns an object with `respond` method that controls how a matched + * request is handled. You can save this object for later use and invoke `respond` again in + * order to change how a matched request is handled. */ /** @@ -1372,13 +1695,16 @@ function createHttpBackendMock($rootScope, $delegate, $browser) { * @description * Creates a new request expectation for POST requests. For more info see `expect()`. * - * @param {string|RegExp} url HTTP url. + * @param {string|RegExp|function(string)} url HTTP url or function that receives a url + * and returns true if the url matches the current definition. * @param {(string|RegExp|function(string)|Object)=} data HTTP request body or function that * receives data string and returns true if the data is as expected, or Object if request body * is in JSON format. * @param {Object=} headers HTTP headers. - * @returns {requestHandler} Returns an object with `respond` method that control how a matched - * request is handled. + * @param {(Array)=} keys Array of keys to assign to regex matches in request url described above. + * @returns {requestHandler} Returns an object with `respond` method that controls how a matched + * request is handled. You can save this object for later use and invoke `respond` again in + * order to change how a matched request is handled. */ /** @@ -1387,13 +1713,16 @@ function createHttpBackendMock($rootScope, $delegate, $browser) { * @description * Creates a new request expectation for PUT requests. For more info see `expect()`. * - * @param {string|RegExp} url HTTP url. + * @param {string|RegExp|function(string)} url HTTP url or function that receives a url + * and returns true if the url matches the current definition. * @param {(string|RegExp|function(string)|Object)=} data HTTP request body or function that * receives data string and returns true if the data is as expected, or Object if request body * is in JSON format. * @param {Object=} headers HTTP headers. - * @returns {requestHandler} Returns an object with `respond` method that control how a matched - * request is handled. + * @param {(Array)=} keys Array of keys to assign to regex matches in request url described above. + * @returns {requestHandler} Returns an object with `respond` method that controls how a matched + * request is handled. You can save this object for later use and invoke `respond` again in + * order to change how a matched request is handled. */ /** @@ -1402,13 +1731,16 @@ function createHttpBackendMock($rootScope, $delegate, $browser) { * @description * Creates a new request expectation for PATCH requests. For more info see `expect()`. * - * @param {string|RegExp} url HTTP url. + * @param {string|RegExp|function(string)} url HTTP url or function that receives a url + * and returns true if the url matches the current definition. * @param {(string|RegExp|function(string)|Object)=} data HTTP request body or function that * receives data string and returns true if the data is as expected, or Object if request body * is in JSON format. * @param {Object=} headers HTTP headers. - * @returns {requestHandler} Returns an object with `respond` method that control how a matched - * request is handled. + * @param {(Array)=} keys Array of keys to assign to regex matches in request url described above. + * @returns {requestHandler} Returns an object with `respond` method that controls how a matched + * request is handled. You can save this object for later use and invoke `respond` again in + * order to change how a matched request is handled. */ /** @@ -1417,12 +1749,32 @@ function createHttpBackendMock($rootScope, $delegate, $browser) { * @description * Creates a new request expectation for JSONP requests. For more info see `expect()`. * - * @param {string|RegExp} url HTTP url. - * @returns {requestHandler} Returns an object with `respond` method that control how a matched - * request is handled. + * @param {string|RegExp|function(string)} url HTTP url or function that receives an url + * and returns true if the url matches the current definition. + * @param {(Array)=} keys Array of keys to assign to regex matches in request url described above. + * @returns {requestHandler} Returns an object with `respond` method that controls how a matched + * request is handled. You can save this object for later use and invoke `respond` again in + * order to change how a matched request is handled. */ createShortMethods('expect'); + /** + * @ngdoc method + * @name $httpBackend#expectRoute + * @description + * Creates a new request expectation that compares only with the requested route. + * + * @param {string} method HTTP method. + * @param {string} url HTTP url string that supports colon param matching. + * @returns {requestHandler} Returns an object with `respond` method that controls how a matched + * request is handled. You can save this object for later use and invoke `respond` again in + * order to change how a matched request is handled. See #expect for more info. + */ + $httpBackend.expectRoute = function(method, url) { + var pathObj = parseRoute(url); + return $httpBackend.expect(method, pathObj.regexp, undefined, undefined, pathObj.keys); + }; + /** * @ngdoc method @@ -1434,11 +1786,11 @@ function createHttpBackendMock($rootScope, $delegate, $browser) { * all pending requests will be flushed. If there are no pending requests when the flush method * is called an exception is thrown (as this typically a sign of programming error). */ - $httpBackend.flush = function(count) { - $rootScope.$digest(); + $httpBackend.flush = function(count, digest) { + if (digest !== false) $rootScope.$digest(); if (!responses.length) throw new Error('No pending request to flush !'); - if (angular.isDefined(count)) { + if (angular.isDefined(count) && count !== null) { while (count--) { if (!responses.length) throw new Error('No more pending request to flush !'); responses.shift()(); @@ -1448,7 +1800,7 @@ function createHttpBackendMock($rootScope, $delegate, $browser) { responses.shift()(); } } - $httpBackend.verifyNoOutstandingExpectation(); + $httpBackend.verifyNoOutstandingExpectation(digest); }; @@ -1466,8 +1818,8 @@ function createHttpBackendMock($rootScope, $delegate, $browser) { * afterEach($httpBackend.verifyNoOutstandingExpectation); * ``` */ - $httpBackend.verifyNoOutstandingExpectation = function() { - $rootScope.$digest(); + $httpBackend.verifyNoOutstandingExpectation = function(digest) { + if (digest !== false) $rootScope.$digest(); if (expectations.length) { throw new Error('Unsatisfied requests: ' + expectations.join(', ')); } @@ -1511,21 +1863,21 @@ function createHttpBackendMock($rootScope, $delegate, $browser) { function createShortMethods(prefix) { - angular.forEach(['GET', 'DELETE', 'JSONP'], function(method) { - $httpBackend[prefix + method] = function(url, headers) { - return $httpBackend[prefix](method, url, undefined, headers); + angular.forEach(['GET', 'DELETE', 'JSONP', 'HEAD'], function(method) { + $httpBackend[prefix + method] = function(url, headers, keys) { + return $httpBackend[prefix](method, url, undefined, headers, keys); }; }); angular.forEach(['PUT', 'POST', 'PATCH'], function(method) { - $httpBackend[prefix + method] = function(url, data, headers) { - return $httpBackend[prefix](method, url, data, headers); + $httpBackend[prefix + method] = function(url, data, headers, keys) { + return $httpBackend[prefix](method, url, data, headers, keys); }; }); } } -function MockHttpExpectation(method, url, data, headers) { +function MockHttpExpectation(method, url, data, headers, keys) { this.data = data; this.headers = headers; @@ -1541,6 +1893,7 @@ function MockHttpExpectation(method, url, data, headers) { this.matchUrl = function(u) { if (!url) return true; if (angular.isFunction(url.test)) return url.test(u); + if (angular.isFunction(url)) return url(u); return url == u; }; @@ -1554,13 +1907,68 @@ function MockHttpExpectation(method, url, data, headers) { if (angular.isUndefined(data)) return true; if (data && angular.isFunction(data.test)) return data.test(d); if (data && angular.isFunction(data)) return data(d); - if (data && !angular.isString(data)) return angular.equals(data, angular.fromJson(d)); + if (data && !angular.isString(data)) { + return angular.equals(angular.fromJson(angular.toJson(data)), angular.fromJson(d)); + } return data == d; }; this.toString = function() { return method + ' ' + url; }; + + this.params = function(u) { + return angular.extend(parseQuery(), pathParams()); + + function pathParams() { + var keyObj = {}; + if (!url || !angular.isFunction(url.test) || !keys || keys.length === 0) return keyObj; + + var m = url.exec(u); + if (!m) return keyObj; + for (var i = 1, len = m.length; i < len; ++i) { + var key = keys[i - 1]; + var val = m[i]; + if (key && val) { + keyObj[key.name || key] = val; + } + } + + return keyObj; + } + + function parseQuery() { + var obj = {}, key_value, key, + queryStr = u.indexOf('?') > -1 + ? u.substring(u.indexOf('?') + 1) + : ""; + + angular.forEach(queryStr.split('&'), function(keyValue) { + if (keyValue) { + key_value = keyValue.replace(/\+/g,'%20').split('='); + key = tryDecodeURIComponent(key_value[0]); + if (angular.isDefined(key)) { + var val = angular.isDefined(key_value[1]) ? tryDecodeURIComponent(key_value[1]) : true; + if (!hasOwnProperty.call(obj, key)) { + obj[key] = val; + } else if (angular.isArray(obj[key])) { + obj[key].push(val); + } else { + obj[key] = [obj[key],val]; + } + } + } + }); + return obj; + } + function tryDecodeURIComponent(value) { + try { + return decodeURIComponent(value); + } catch (e) { + // Ignore any invalid uri component + } + } + }; } function createMockXhr() { @@ -1615,6 +2023,20 @@ function MockXhr() { }; this.abort = angular.noop; + + // This section simulates the events on a real XHR object (and the upload object) + // When we are testing $httpBackend (inside the angular project) we make partial use of this + // but store the events directly ourselves on `$$events`, instead of going through the `addEventListener` + this.$$events = {}; + this.addEventListener = function(name, listener) { + if (angular.isUndefined(this.$$events[name])) this.$$events[name] = []; + this.$$events[name].push(listener); + }; + + this.upload = { + $$events: {}, + addEventListener: this.addEventListener + }; } @@ -1627,7 +2049,7 @@ function MockXhr() { * that adds a "flush" and "verifyNoPendingTasks" methods. */ -angular.mock.$TimeoutDecorator = function($delegate, $browser) { +angular.mock.$TimeoutDecorator = ['$delegate', '$browser', function($delegate, $browser) { /** * @ngdoc method @@ -1666,59 +2088,162 @@ angular.mock.$TimeoutDecorator = function($delegate, $browser) { } return $delegate; -}; +}]; -angular.mock.$RAFDecorator = function($delegate) { - var queue = []; +angular.mock.$RAFDecorator = ['$delegate', function($delegate) { var rafFn = function(fn) { - var index = queue.length; - queue.push(fn); + var index = rafFn.queue.length; + rafFn.queue.push(fn); return function() { - queue.splice(index, 1); + rafFn.queue.splice(index, 1); }; }; + rafFn.queue = []; rafFn.supported = $delegate.supported; rafFn.flush = function() { - if(queue.length === 0) { + if (rafFn.queue.length === 0) { throw new Error('No rAF callbacks present'); } - var length = queue.length; - for(var i=0;i'); - }; + this.$get = ['$injector', function($injector) { + originalRootElement = angular.element('
        ').data('$injector', $injector); + return originalRootElement; + }]; }; +/** + * @ngdoc service + * @name $controller + * @description + * A decorator for {@link ng.$controller} with additional `bindings` parameter, useful when testing + * controllers of directives that use {@link $compile#-bindtocontroller- `bindToController`}. + * + * + * ## Example + * + * ```js + * + * // Directive definition ... + * + * myMod.directive('myDirective', { + * controller: 'MyDirectiveController', + * bindToController: { + * name: '@' + * } + * }); + * + * + * // Controller definition ... + * + * myMod.controller('MyDirectiveController', ['$log', function($log) { + * $log.info(this.name); + * }]); + * + * + * // In a test ... + * + * describe('myDirectiveController', function() { + * it('should write the bound name to the log', inject(function($controller, $log) { + * var ctrl = $controller('MyDirectiveController', { /* no locals */ }, { name: 'Clark Kent' }); + * expect(ctrl.name).toEqual('Clark Kent'); + * expect($log.info.logs).toEqual(['Clark Kent']); + * })); + * }); + * + * ``` + * + * @param {Function|string} constructor If called with a function then it's considered to be the + * controller constructor function. Otherwise it's considered to be a string which is used + * to retrieve the controller constructor using the following steps: + * + * * check if a controller with given name is registered via `$controllerProvider` + * * check if evaluating the string on the current scope returns a constructor + * * if $controllerProvider#allowGlobals, check `window[constructor]` on the global + * `window` object (not recommended) + * + * The string can use the `controller as property` syntax, where the controller instance is published + * as the specified property on the `scope`; the `scope` must be injected into `locals` param for this + * to work correctly. + * + * @param {Object} locals Injection locals for Controller. + * @param {Object=} bindings Properties to add to the controller before invoking the constructor. This is used + * to simulate the `bindToController` feature and simplify certain kinds of tests. + * @return {Object} Instance of given controller. + */ +angular.mock.$ControllerDecorator = ['$delegate', function($delegate) { + return function(expression, locals, later, ident) { + if (later && typeof later === 'object') { + var create = $delegate(expression, locals, true, ident); + angular.extend(create.instance, later); + return create(); + } + return $delegate(expression, locals, later, ident); + }; +}]; + +/** + * @ngdoc service + * @name $componentController + * @description + * A service that can be used to create instances of component controllers. + *
        + * Be aware that the controller will be instantiated and attached to the scope as specified in + * the component definition object. If you do not provide a `$scope` object in the `locals` param + * then the helper will create a new isolated scope as a child of `$rootScope`. + *
        + * @param {string} componentName the name of the component whose controller we want to instantiate + * @param {Object} locals Injection locals for Controller. + * @param {Object=} bindings Properties to add to the controller before invoking the constructor. This is used + * to simulate the `bindToController` feature and simplify certain kinds of tests. + * @param {string=} ident Override the property name to use when attaching the controller to the scope. + * @return {Object} Instance of requested controller. + */ +angular.mock.$ComponentControllerProvider = ['$compileProvider', function($compileProvider) { + this.$get = ['$controller','$injector', '$rootScope', function($controller, $injector, $rootScope) { + return function $componentController(componentName, locals, bindings, ident) { + // get all directives associated to the component name + var directives = $injector.get(componentName + 'Directive'); + // look for those directives that are components + var candidateDirectives = directives.filter(function(directiveInfo) { + // components have controller, controllerAs and restrict:'E' + return directiveInfo.controller && directiveInfo.controllerAs && directiveInfo.restrict === 'E'; + }); + // check if valid directives found + if (candidateDirectives.length === 0) { + throw new Error('No component found'); + } + if (candidateDirectives.length > 1) { + throw new Error('Too many components found'); + } + // get the info of the component + var directiveInfo = candidateDirectives[0]; + // create a scope if needed + locals = locals || {}; + locals.$scope = locals.$scope || $rootScope.$new(true); + return $controller(directiveInfo.controller, locals, bindings, ident || directiveInfo.controllerAs); + }; + }]; +}]; + + /** * @ngdoc module * @name ngMock @@ -1734,6 +2259,48 @@ angular.mock.$RootElementProvider = function() { * *
        * + * @installation + * + *

        First, download the file:

        + *
          +
        • + Google CDN e.g. + {% code %}"//ajax.googleapis.com/ajax/libs/angularjs/X.Y.Z/"{% endcode %} +
        • +
        • + NPM e.g. + {% code %}npm install {$ doc.packageName $}@X.Y.Z{% endcode %} +
        • +
        • + Bower
          e.g. + {% code %}bower install {$ doc.packageName $}@X.Y.Z{% endcode %} +
        • +
        • + code.angularjs.org (discouraged for + production use) e.g. + {% code %}"//code.angularjs.org/X.Y.Z/{$ doc.packageFile $}"{% endcode %} +
        • +
        +

        where X.Y.Z is the AngularJS version you are running.

        + +

        Then, configure your test runner to load `angular-mocks.js` after `angular.js`. + This example uses Karma:

        + + {% code %} + config.set({ + files: [ + 'build/angular.js', // and other module files you need + 'build/angular-mocks.js', + '', + '' + ] + }); + {% endcode %} + +

        Including the `angular-mocks.js` file automatically adds the `ngMock` module, so your tests + are ready to go!

        + * + * */ angular.module('ngMock', ['ng']).provider({ $browser: angular.mock.$BrowserProvider, @@ -1741,11 +2308,13 @@ angular.module('ngMock', ['ng']).provider({ $log: angular.mock.$LogProvider, $interval: angular.mock.$IntervalProvider, $httpBackend: angular.mock.$HttpBackendProvider, - $rootElement: angular.mock.$RootElementProvider + $rootElement: angular.mock.$RootElementProvider, + $componentController: angular.mock.$ComponentControllerProvider }).config(['$provide', function($provide) { $provide.decorator('$timeout', angular.mock.$TimeoutDecorator); $provide.decorator('$$rAF', angular.mock.$RAFDecorator); - $provide.decorator('$$asyncCallback', angular.mock.$AsyncCallbackDecorator); + $provide.decorator('$rootScope', angular.mock.$RootScopeDecorator); + $provide.decorator('$controller', angular.mock.$ControllerDecorator); }]); /** @@ -1771,8 +2340,10 @@ angular.module('ngMockE2E', ['ng']).config(['$provide', function($provide) { * Fake HTTP backend implementation suitable for end-to-end testing or backend-less development of * applications that use the {@link ng.$http $http service}. * - * *Note*: For fake http backend implementation suitable for unit testing please see + *
        + * **Note**: For fake http backend implementation suitable for unit testing please see * {@link ngMock.$httpBackend unit-testing $httpBackend mock}. + *
        * * This implementation can be used to respond with static or dynamic responses via the `when` api * and its shortcuts (`whenGET`, `whenPOST`, etc) and optionally pass through requests to the @@ -1786,16 +2357,16 @@ angular.module('ngMockE2E', ['ng']).config(['$provide', function($provide) { * use the `passThrough` request handler of `when` instead of `respond`. * * Additionally, we don't want to manually have to flush mocked out requests like we do during unit - * testing. For this reason the e2e $httpBackend automatically flushes mocked out requests + * testing. For this reason the e2e $httpBackend flushes mocked out requests * automatically, closely simulating the behavior of the XMLHttpRequest object. * * To setup the application to run with this http backend, you have to create a module that depends * on the `ngMockE2E` and your application modules and defines the fake backend: * * ```js - * myAppDev = angular.module('myAppDev', ['myApp', 'ngMockE2E']); + * var myAppDev = angular.module('myAppDev', ['myApp', 'ngMockE2E']); * myAppDev.run(function($httpBackend) { - * phones = [{name: 'phone1'}, {name: 'phone2'}]; + * var phones = [{name: 'phone1'}, {name: 'phone2'}]; * * // returns the current list of phones * $httpBackend.whenGET('/phones').respond(phones); @@ -1806,12 +2377,74 @@ angular.module('ngMockE2E', ['ng']).config(['$provide', function($provide) { * phones.push(phone); * return [200, phone, {}]; * }); - * $httpBackend.whenGET(/^\/templates\//).passThrough(); + * $httpBackend.whenGET(/^\/templates\//).passThrough(); // Requests for templates are handled by the real server * //... * }); * ``` * * Afterwards, bootstrap your app with this new module. + * + * ## Example + * + * + * var myApp = angular.module('myApp', []); + * + * myApp.controller('main', function($http) { + * var ctrl = this; + * + * ctrl.phones = []; + * ctrl.newPhone = { + * name: '' + * }; + * + * ctrl.getPhones = function() { + * $http.get('/phones').then(function(response) { + * ctrl.phones = response.data; + * }); + * }; + * + * ctrl.addPhone = function(phone) { + * $http.post('/phones', phone).then(function() { + * ctrl.newPhone = {name: ''}; + * return ctrl.getPhones(); + * }); + * }; + * + * ctrl.getPhones(); + * }); + * + * + * var myAppDev = angular.module('myAppE2E', ['myApp', 'ngMockE2E']); + * + * myAppDev.run(function($httpBackend) { + * var phones = [{name: 'phone1'}, {name: 'phone2'}]; + * + * // returns the current list of phones + * $httpBackend.whenGET('/phones').respond(phones); + * + * // adds a new phone to the phones array + * $httpBackend.whenPOST('/phones').respond(function(method, url, data) { + * var phone = angular.fromJson(data); + * phones.push(phone); + * return [200, phone, {}]; + * }); + * }); + * + * + *
        + *
        + * + * + *
        + *

        Phones

        + *
          + *
        • {{phone.name}}
        • + *
        + *
        + *
        + *
        + * + * */ /** @@ -1822,22 +2455,29 @@ angular.module('ngMockE2E', ['ng']).config(['$provide', function($provide) { * Creates a new backend definition. * * @param {string} method HTTP method. - * @param {string|RegExp} url HTTP url. + * @param {string|RegExp|function(string)} url HTTP url or function that receives a url + * and returns true if the url matches the current definition. * @param {(string|RegExp)=} data HTTP request body. * @param {(Object|function(Object))=} headers HTTP headers or function that receives http header * object and returns true if the headers match the current definition. + * @param {(Array)=} keys Array of keys to assign to regex matches in request url described on + * {@link ngMock.$httpBackend $httpBackend mock}. * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that - * control how a matched request is handled. + * control how a matched request is handled. You can save this object for later use and invoke + * `respond` or `passThrough` again in order to change how a matched request is handled. * * - respond – - * `{function([status,] data[, headers, statusText]) - * | function(function(method, url, data, headers)}` + * ``` + * { function([status,] data[, headers, statusText]) + * | function(function(method, url, data, headers, params)} + * ``` * – The respond method takes a set of static data to be returned or a function that can return - * an array containing response status (number), response data (string), response headers - * (Object), and the text for the status (string). + * an array containing response status (number), response data (Array|Object|string), response + * headers (Object), and the text for the status (string). * - passThrough – `{function()}` – Any request matching a backend definition with * `passThrough` handler will be passed through to the real backend (an XHR request will be made * to the server.) + * - Both methods return the `requestHandler` object for possible overrides. */ /** @@ -1847,10 +2487,14 @@ angular.module('ngMockE2E', ['ng']).config(['$provide', function($provide) { * @description * Creates a new backend definition for GET requests. For more info see `when()`. * - * @param {string|RegExp} url HTTP url. + * @param {string|RegExp|function(string)} url HTTP url or function that receives a url + * and returns true if the url matches the current definition. * @param {(Object|function(Object))=} headers HTTP headers. + * @param {(Array)=} keys Array of keys to assign to regex matches in request url described on + * {@link ngMock.$httpBackend $httpBackend mock}. * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that - * control how a matched request is handled. + * control how a matched request is handled. You can save this object for later use and invoke + * `respond` or `passThrough` again in order to change how a matched request is handled. */ /** @@ -1860,10 +2504,14 @@ angular.module('ngMockE2E', ['ng']).config(['$provide', function($provide) { * @description * Creates a new backend definition for HEAD requests. For more info see `when()`. * - * @param {string|RegExp} url HTTP url. + * @param {string|RegExp|function(string)} url HTTP url or function that receives a url + * and returns true if the url matches the current definition. * @param {(Object|function(Object))=} headers HTTP headers. + * @param {(Array)=} keys Array of keys to assign to regex matches in request url described on + * {@link ngMock.$httpBackend $httpBackend mock}. * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that - * control how a matched request is handled. + * control how a matched request is handled. You can save this object for later use and invoke + * `respond` or `passThrough` again in order to change how a matched request is handled. */ /** @@ -1873,10 +2521,14 @@ angular.module('ngMockE2E', ['ng']).config(['$provide', function($provide) { * @description * Creates a new backend definition for DELETE requests. For more info see `when()`. * - * @param {string|RegExp} url HTTP url. + * @param {string|RegExp|function(string)} url HTTP url or function that receives a url + * and returns true if the url matches the current definition. * @param {(Object|function(Object))=} headers HTTP headers. + * @param {(Array)=} keys Array of keys to assign to regex matches in request url described on + * {@link ngMock.$httpBackend $httpBackend mock}. * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that - * control how a matched request is handled. + * control how a matched request is handled. You can save this object for later use and invoke + * `respond` or `passThrough` again in order to change how a matched request is handled. */ /** @@ -1886,11 +2538,15 @@ angular.module('ngMockE2E', ['ng']).config(['$provide', function($provide) { * @description * Creates a new backend definition for POST requests. For more info see `when()`. * - * @param {string|RegExp} url HTTP url. + * @param {string|RegExp|function(string)} url HTTP url or function that receives a url + * and returns true if the url matches the current definition. * @param {(string|RegExp)=} data HTTP request body. * @param {(Object|function(Object))=} headers HTTP headers. + * @param {(Array)=} keys Array of keys to assign to regex matches in request url described on + * {@link ngMock.$httpBackend $httpBackend mock}. * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that - * control how a matched request is handled. + * control how a matched request is handled. You can save this object for later use and invoke + * `respond` or `passThrough` again in order to change how a matched request is handled. */ /** @@ -1900,11 +2556,15 @@ angular.module('ngMockE2E', ['ng']).config(['$provide', function($provide) { * @description * Creates a new backend definition for PUT requests. For more info see `when()`. * - * @param {string|RegExp} url HTTP url. + * @param {string|RegExp|function(string)} url HTTP url or function that receives a url + * and returns true if the url matches the current definition. * @param {(string|RegExp)=} data HTTP request body. * @param {(Object|function(Object))=} headers HTTP headers. + * @param {(Array)=} keys Array of keys to assign to regex matches in request url described on + * {@link ngMock.$httpBackend $httpBackend mock}. * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that - * control how a matched request is handled. + * control how a matched request is handled. You can save this object for later use and invoke + * `respond` or `passThrough` again in order to change how a matched request is handled. */ /** @@ -1914,11 +2574,15 @@ angular.module('ngMockE2E', ['ng']).config(['$provide', function($provide) { * @description * Creates a new backend definition for PATCH requests. For more info see `when()`. * - * @param {string|RegExp} url HTTP url. + * @param {string|RegExp|function(string)} url HTTP url or function that receives a url + * and returns true if the url matches the current definition. * @param {(string|RegExp)=} data HTTP request body. * @param {(Object|function(Object))=} headers HTTP headers. + * @param {(Array)=} keys Array of keys to assign to regex matches in request url described on + * {@link ngMock.$httpBackend $httpBackend mock}. * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that - * control how a matched request is handled. + * control how a matched request is handled. You can save this object for later use and invoke + * `respond` or `passThrough` again in order to change how a matched request is handled. */ /** @@ -1928,45 +2592,304 @@ angular.module('ngMockE2E', ['ng']).config(['$provide', function($provide) { * @description * Creates a new backend definition for JSONP requests. For more info see `when()`. * - * @param {string|RegExp} url HTTP url. + * @param {string|RegExp|function(string)} url HTTP url or function that receives a url + * and returns true if the url matches the current definition. + * @param {(Array)=} keys Array of keys to assign to regex matches in request url described on + * {@link ngMock.$httpBackend $httpBackend mock}. * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that - * control how a matched request is handled. + * control how a matched request is handled. You can save this object for later use and invoke + * `respond` or `passThrough` again in order to change how a matched request is handled. + */ +/** + * @ngdoc method + * @name $httpBackend#whenRoute + * @module ngMockE2E + * @description + * Creates a new backend definition that compares only with the requested route. + * + * @param {string} method HTTP method. + * @param {string} url HTTP url string that supports colon param matching. + * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that + * control how a matched request is handled. You can save this object for later use and invoke + * `respond` or `passThrough` again in order to change how a matched request is handled. */ angular.mock.e2e = {}; angular.mock.e2e.$httpBackendDecorator = - ['$rootScope', '$delegate', '$browser', createHttpBackendMock]; + ['$rootScope', '$timeout', '$delegate', '$browser', createHttpBackendMock]; -angular.mock.clearDataCache = function() { - var key, - cache = angular.element.cache; +/** + * @ngdoc type + * @name $rootScope.Scope + * @module ngMock + * @description + * {@link ng.$rootScope.Scope Scope} type decorated with helper methods useful for testing. These + * methods are automatically available on any {@link ng.$rootScope.Scope Scope} instance when + * `ngMock` module is loaded. + * + * In addition to all the regular `Scope` methods, the following helper methods are available: + */ +angular.mock.$RootScopeDecorator = ['$delegate', function($delegate) { - for(key in cache) { - if (Object.prototype.hasOwnProperty.call(cache,key)) { - var handle = cache[key].handle; + var $rootScopePrototype = Object.getPrototypeOf($delegate); - handle && angular.element(handle.elem).off(); - delete cache[key]; + $rootScopePrototype.$countChildScopes = countChildScopes; + $rootScopePrototype.$countWatchers = countWatchers; + + return $delegate; + + // ------------------------------------------------------------------------------------------ // + + /** + * @ngdoc method + * @name $rootScope.Scope#$countChildScopes + * @module ngMock + * @description + * Counts all the direct and indirect child scopes of the current scope. + * + * The current scope is excluded from the count. The count includes all isolate child scopes. + * + * @returns {number} Total number of child scopes. + */ + function countChildScopes() { + // jshint validthis: true + var count = 0; // exclude the current scope + var pendingChildHeads = [this.$$childHead]; + var currentScope; + + while (pendingChildHeads.length) { + currentScope = pendingChildHeads.shift(); + + while (currentScope) { + count += 1; + pendingChildHeads.push(currentScope.$$childHead); + currentScope = currentScope.$$nextSibling; + } } + + return count; } -}; -if(window.jasmine || window.mocha) { + /** + * @ngdoc method + * @name $rootScope.Scope#$countWatchers + * @module ngMock + * @description + * Counts all the watchers of direct and indirect child scopes of the current scope. + * + * The watchers of the current scope are included in the count and so are all the watchers of + * isolate child scopes. + * + * @returns {number} Total number of watchers. + */ + function countWatchers() { + // jshint validthis: true + var count = this.$$watchers ? this.$$watchers.length : 0; // include the current scope + var pendingChildHeads = [this.$$childHead]; + var currentScope; + + while (pendingChildHeads.length) { + currentScope = pendingChildHeads.shift(); + + while (currentScope) { + count += currentScope.$$watchers ? currentScope.$$watchers.length : 0; + pendingChildHeads.push(currentScope.$$childHead); + currentScope = currentScope.$$nextSibling; + } + } + + return count; + } +}]; + + +!(function(jasmineOrMocha) { + + if (!jasmineOrMocha) { + return; + } var currentSpec = null, - isSpecRunning = function() { + injectorState = new InjectorState(), + annotatedFunctions = [], + wasInjectorCreated = function() { return !!currentSpec; }; + angular.mock.$$annotate = angular.injector.$$annotate; + angular.injector.$$annotate = function(fn) { + if (typeof fn === 'function' && !fn.$inject) { + annotatedFunctions.push(fn); + } + return angular.mock.$$annotate.apply(this, arguments); + }; - (window.beforeEach || window.setup)(function() { - currentSpec = this; - }); + /** + * @ngdoc function + * @name angular.mock.module + * @description + * + * *NOTE*: This function is also published on window for easy access.
        + * *NOTE*: This function is declared ONLY WHEN running tests with jasmine or mocha + * + * This function registers a module configuration code. It collects the configuration information + * which will be used when the injector is created by {@link angular.mock.inject inject}. + * + * See {@link angular.mock.inject inject} for usage example + * + * @param {...(string|Function|Object)} fns any number of modules which are represented as string + * aliases or as anonymous module initialization functions. The modules are used to + * configure the injector. The 'ng' and 'ngMock' modules are automatically loaded. If an + * object literal is passed each key-value pair will be registered on the module via + * {@link auto.$provide $provide}.value, the key being the string name (or token) to associate + * with the value on the injector. + */ + var module = window.module = angular.mock.module = function() { + var moduleFns = Array.prototype.slice.call(arguments, 0); + return wasInjectorCreated() ? workFn() : workFn; + ///////////////////// + function workFn() { + if (currentSpec.$injector) { + throw new Error('Injector already created, can not register a module!'); + } else { + var fn, modules = currentSpec.$modules || (currentSpec.$modules = []); + angular.forEach(moduleFns, function(module) { + if (angular.isObject(module) && !angular.isArray(module)) { + fn = ['$provide', function($provide) { + angular.forEach(module, function(value, key) { + $provide.value(key, value); + }); + }]; + } else { + fn = module; + } + if (currentSpec.$providerInjector) { + currentSpec.$providerInjector.invoke(fn); + } else { + modules.push(fn); + } + }); + } + } + }; - (window.afterEach || window.teardown)(function() { + module.$$beforeAllHook = (window.before || window.beforeAll); + module.$$afterAllHook = (window.after || window.afterAll); + + // purely for testing ngMock itself + module.$$currentSpec = function(to) { + if (arguments.length === 0) return to; + currentSpec = to; + }; + + /** + * @ngdoc function + * @name angular.mock.module.sharedInjector + * @description + * + * *NOTE*: This function is declared ONLY WHEN running tests with jasmine or mocha + * + * This function ensures a single injector will be used for all tests in a given describe context. + * This contrasts with the default behaviour where a new injector is created per test case. + * + * Use sharedInjector when you want to take advantage of Jasmine's `beforeAll()`, or mocha's + * `before()` methods. Call `module.sharedInjector()` before you setup any other hooks that + * will create (i.e call `module()`) or use (i.e call `inject()`) the injector. + * + * You cannot call `sharedInjector()` from within a context already using `sharedInjector()`. + * + * ## Example + * + * Typically beforeAll is used to make many assertions about a single operation. This can + * cut down test run-time as the test setup doesn't need to be re-run, and enabling focussed + * tests each with a single assertion. + * + * ```js + * describe("Deep Thought", function() { + * + * module.sharedInjector(); + * + * beforeAll(module("UltimateQuestion")); + * + * beforeAll(inject(function(DeepThought) { + * expect(DeepThought.answer).toBeUndefined(); + * DeepThought.generateAnswer(); + * })); + * + * it("has calculated the answer correctly", inject(function(DeepThought) { + * // Because of sharedInjector, we have access to the instance of the DeepThought service + * // that was provided to the beforeAll() hook. Therefore we can test the generated answer + * expect(DeepThought.answer).toBe(42); + * })); + * + * it("has calculated the answer within the expected time", inject(function(DeepThought) { + * expect(DeepThought.runTimeMillennia).toBeLessThan(8000); + * })); + * + * it("has double checked the answer", inject(function(DeepThought) { + * expect(DeepThought.absolutelySureItIsTheRightAnswer).toBe(true); + * })); + * + * }); + * + * ``` + */ + module.sharedInjector = function() { + if (!(module.$$beforeAllHook && module.$$afterAllHook)) { + throw Error("sharedInjector() cannot be used unless your test runner defines beforeAll/afterAll"); + } + + var initialized = false; + + module.$$beforeAllHook(function() { + if (injectorState.shared) { + injectorState.sharedError = Error("sharedInjector() cannot be called inside a context that has already called sharedInjector()"); + throw injectorState.sharedError; + } + initialized = true; + currentSpec = this; + injectorState.shared = true; + }); + + module.$$afterAllHook(function() { + if (initialized) { + injectorState = new InjectorState(); + module.$$cleanup(); + } else { + injectorState.sharedError = null; + } + }); + }; + + module.$$beforeEach = function() { + if (injectorState.shared && currentSpec && currentSpec != this) { + var state = currentSpec; + currentSpec = this; + angular.forEach(["$injector","$modules","$providerInjector", "$injectorStrict"], function(k) { + currentSpec[k] = state[k]; + state[k] = null; + }); + } else { + currentSpec = this; + originalRootElement = null; + annotatedFunctions = []; + } + }; + + module.$$afterEach = function() { + if (injectorState.cleanupAfterEach()) { + module.$$cleanup(); + } + }; + + module.$$cleanup = function() { var injector = currentSpec.$injector; + annotatedFunctions.forEach(function(fn) { + delete fn.$inject; + }); + angular.forEach(currentSpec.$modules, function(module) { if (module && module.$$hashKey) { module.$$hashKey = undefined; @@ -1975,14 +2898,24 @@ if(window.jasmine || window.mocha) { currentSpec.$injector = null; currentSpec.$modules = null; + currentSpec.$providerInjector = null; currentSpec = null; if (injector) { - injector.get('$rootElement').off(); - injector.get('$browser').pollFns.length = 0; - } + // Ensure `$rootElement` is instantiated, before checking `originalRootElement` + var $rootElement = injector.get('$rootElement'); + var rootNode = $rootElement && $rootElement[0]; + var cleanUpNodes = !originalRootElement ? [] : [originalRootElement[0]]; + if (rootNode && (!originalRootElement || rootNode !== originalRootElement[0])) { + cleanUpNodes.push(rootNode); + } + angular.element.cleanData(cleanUpNodes); - angular.mock.clearDataCache(); + // Ensure `$destroy()` is available, before calling it + // (a mocked `$rootScope` might not implement it (or not even be an object at all)) + var $rootScope = injector.get('$rootScope'); + if ($rootScope && $rootScope.$destroy) $rootScope.$destroy(); + } // clean up jquery's fragment cache angular.forEach(angular.element.fragments, function(val, key) { @@ -1995,56 +2928,18 @@ if(window.jasmine || window.mocha) { delete angular.callbacks[key]; }); angular.callbacks.counter = 0; - }); - - /** - * @ngdoc function - * @name angular.mock.module - * @description - * - * *NOTE*: This function is also published on window for easy access.
        - * - * This function registers a module configuration code. It collects the configuration information - * which will be used when the injector is created by {@link angular.mock.inject inject}. - * - * See {@link angular.mock.inject inject} for usage example - * - * @param {...(string|Function|Object)} fns any number of modules which are represented as string - * aliases or as anonymous module initialization functions. The modules are used to - * configure the injector. The 'ng' and 'ngMock' modules are automatically loaded. If an - * object literal is passed they will be registered as values in the module, the key being - * the module name and the value being what is returned. - */ - window.module = angular.mock.module = function() { - var moduleFns = Array.prototype.slice.call(arguments, 0); - return isSpecRunning() ? workFn() : workFn; - ///////////////////// - function workFn() { - if (currentSpec.$injector) { - throw new Error('Injector already created, can not register a module!'); - } else { - var modules = currentSpec.$modules || (currentSpec.$modules = []); - angular.forEach(moduleFns, function(module) { - if (angular.isObject(module) && !angular.isArray(module)) { - modules.push(function($provide) { - angular.forEach(module, function(value, key) { - $provide.value(key, value); - }); - }); - } else { - modules.push(module); - } - }); - } - } }; + (window.beforeEach || window.setup)(module.$$beforeEach); + (window.afterEach || window.teardown)(module.$$afterEach); + /** * @ngdoc function * @name angular.mock.inject * @description * * *NOTE*: This function is also published on window for easy access.
        + * *NOTE*: This function is declared ONLY WHEN running tests with jasmine or mocha * * The inject function wraps a function into an injectable function. The inject() creates new * instance of {@link auto.$injector $injector} per test, which is then used for @@ -2140,18 +3035,41 @@ if(window.jasmine || window.mocha) { window.inject = angular.mock.inject = function() { var blockFns = Array.prototype.slice.call(arguments, 0); var errorForStack = new Error('Declaration Location'); - return isSpecRunning() ? workFn.call(currentSpec) : workFn; + // IE10+ and PhanthomJS do not set stack trace information, until the error is thrown + if (!errorForStack.stack) { + try { + throw errorForStack; + } catch (e) {} + } + return wasInjectorCreated() ? workFn.call(currentSpec) : workFn; ///////////////////// function workFn() { var modules = currentSpec.$modules || []; - + var strictDi = !!currentSpec.$injectorStrict; + modules.unshift(['$injector', function($injector) { + currentSpec.$providerInjector = $injector; + }]); modules.unshift('ngMock'); modules.unshift('ng'); var injector = currentSpec.$injector; if (!injector) { - injector = currentSpec.$injector = angular.injector(modules); + if (strictDi) { + // If strictDi is enabled, annotate the providerInjector blocks + angular.forEach(modules, function(moduleFn) { + if (typeof moduleFn === "function") { + angular.injector.$$annotate(moduleFn); + } + }); + } + injector = currentSpec.$injector = angular.injector(modules, strictDi); + currentSpec.$injectorStrict = strictDi; } - for(var i = 0, ii = blockFns.length; i < ii; i++) { + for (var i = 0, ii = blockFns.length; i < ii; i++) { + if (currentSpec.$injectorStrict) { + // If the injector is strict / strictDi, and the spec wants to inject using automatic + // annotation, then annotate the function here. + injector.annotate(blockFns[i]); + } try { /* jshint -W040 *//* Jasmine explicitly provides a `this` object when calling functions */ injector.invoke(blockFns[i] || angular.noop, this); @@ -2167,7 +3085,32 @@ if(window.jasmine || window.mocha) { } } }; -} + + + angular.mock.inject.strictDi = function(value) { + value = arguments.length ? !!value : true; + return wasInjectorCreated() ? workFn() : workFn; + + function workFn() { + if (value !== currentSpec.$injectorStrict) { + if (currentSpec.$injector) { + throw new Error('Injector already created, can not modify strict annotations'); + } else { + currentSpec.$injectorStrict = value; + } + } + } + }; + + function InjectorState() { + this.shared = false; + this.sharedError = null; + + this.cleanupAfterEach = function() { + return !this.shared || this.sharedError; + }; + } +})(window.jasmine || window.mocha); })(window, window.angular); diff --git a/app/bower_components/angular-mocks/bower.json b/app/bower_components/angular-mocks/bower.json index 5ad409f..57a1c04 100644 --- a/app/bower_components/angular-mocks/bower.json +++ b/app/bower_components/angular-mocks/bower.json @@ -1,8 +1,10 @@ { "name": "angular-mocks", - "version": "1.2.21", + "version": "1.5.6-build.4801+sha.3b5751d", + "license": "MIT", "main": "./angular-mocks.js", + "ignore": [], "dependencies": { - "angular": "1.2.21" + "angular": "1.5.6-build.4801+sha.3b5751d" } } diff --git a/app/bower_components/angular-mocks/ngAnimateMock.js b/app/bower_components/angular-mocks/ngAnimateMock.js new file mode 100644 index 0000000..6f99e62 --- /dev/null +++ b/app/bower_components/angular-mocks/ngAnimateMock.js @@ -0,0 +1,2 @@ +require('./angular-mocks'); +module.exports = 'ngAnimateMock'; diff --git a/app/bower_components/angular-mocks/ngMock.js b/app/bower_components/angular-mocks/ngMock.js new file mode 100644 index 0000000..7944de7 --- /dev/null +++ b/app/bower_components/angular-mocks/ngMock.js @@ -0,0 +1,2 @@ +require('./angular-mocks'); +module.exports = 'ngMock'; diff --git a/app/bower_components/angular-mocks/ngMockE2E.js b/app/bower_components/angular-mocks/ngMockE2E.js new file mode 100644 index 0000000..fc2e539 --- /dev/null +++ b/app/bower_components/angular-mocks/ngMockE2E.js @@ -0,0 +1,2 @@ +require('./angular-mocks'); +module.exports = 'ngMockE2E'; diff --git a/app/bower_components/angular-mocks/package.json b/app/bower_components/angular-mocks/package.json new file mode 100644 index 0000000..1839133 --- /dev/null +++ b/app/bower_components/angular-mocks/package.json @@ -0,0 +1,27 @@ +{ + "name": "angular-mocks", + "version": "1.5.6-build.4801+sha.3b5751d", + "description": "AngularJS mocks for testing", + "main": "angular-mocks.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "repository": { + "type": "git", + "url": "https://github.com/angular/angular.js.git" + }, + "keywords": [ + "angular", + "framework", + "browser", + "mocks", + "testing", + "client-side" + ], + "author": "Angular Core Team ", + "license": "MIT", + "bugs": { + "url": "https://github.com/angular/angular.js/issues" + }, + "homepage": "http://angularjs.org" +} diff --git a/app/bower_components/angular-route/.bower.json b/app/bower_components/angular-route/.bower.json index 10cb905..675f30d 100644 --- a/app/bower_components/angular-route/.bower.json +++ b/app/bower_components/angular-route/.bower.json @@ -1,18 +1,20 @@ { "name": "angular-route", - "version": "1.2.21", + "version": "1.5.6-build.4801+sha.3b5751d", + "license": "MIT", "main": "./angular-route.js", + "ignore": [], "dependencies": { - "angular": "1.2.21" + "angular": "1.5.6-build.4801+sha.3b5751d" }, "homepage": "https://github.com/angular/bower-angular-route", - "_release": "1.2.21", + "_release": "1.5.6-build.4801+sha.3b5751d", "_resolution": { "type": "version", - "tag": "v1.2.21", - "commit": "a3f4ff7700330ef8e1e84b31177fa1b2206884c8" + "tag": "v1.5.6-build.4801+sha.3b5751d", + "commit": "c3aa1161a8e208b335315ef18681b63ff81df18e" }, - "_source": "git://github.com/angular/bower-angular-route.git", - "_target": "~1.2.21", + "_source": "https://github.com/angular/bower-angular-route.git", + "_target": "~1.5.6", "_originalSource": "angular-route" } \ No newline at end of file diff --git a/app/bower_components/angular-route/README.md b/app/bower_components/angular-route/README.md index 49f55fc..2cd4f90 100644 --- a/app/bower_components/angular-route/README.md +++ b/app/bower_components/angular-route/README.md @@ -1,12 +1,26 @@ -# bower-angular-route +# packaged angular-route -This repo is for distribution on `bower`. The source for this module is in the +This repo is for distribution on `npm` and `bower`. The source for this module is in the [main AngularJS repo](https://github.com/angular/angular.js/tree/master/src/ngRoute). Please file issues and pull requests against that repo. ## Install -Install with `bower`: +You can install this package either with `npm` or with `bower`. + +### npm + +```shell +npm install angular-route +``` + +Then add `ngRoute` as a dependency for your app: + +```javascript +angular.module('myApp', [require('angular-route')]); +``` + +### bower ```shell bower install angular-route @@ -18,7 +32,7 @@ Add a ` ``` -And add `ngRoute` as a dependency for your app: +Then add `ngRoute` as a dependency for your app: ```javascript angular.module('myApp', ['ngRoute']); @@ -33,7 +47,7 @@ Documentation is available on the The MIT License -Copyright (c) 2010-2012 Google, Inc. http://angularjs.org +Copyright (c) 2010-2015 Google, Inc. http://angularjs.org Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/app/bower_components/angular-route/angular-route.js b/app/bower_components/angular-route/angular-route.js index a589753..4eee4f9 100644 --- a/app/bower_components/angular-route/angular-route.js +++ b/app/bower_components/angular-route/angular-route.js @@ -1,9 +1,9 @@ /** - * @license AngularJS v1.2.21 - * (c) 2010-2014 Google, Inc. http://angularjs.org + * @license AngularJS v1.5.6-build.4801+sha.3b5751d + * (c) 2010-2016 Google, Inc. http://angularjs.org * License: MIT */ -(function(window, angular, undefined) {'use strict'; +(function(window, angular) {'use strict'; /** * @ngdoc module @@ -22,12 +22,12 @@ */ /* global -ngRouteModule */ var ngRouteModule = angular.module('ngRoute', ['ng']). - provider('$route', $RouteProvider); + provider('$route', $RouteProvider), + $routeMinErr = angular.$$minErr('ngRoute'); /** * @ngdoc provider * @name $routeProvider - * @kind function * * @description * @@ -39,9 +39,9 @@ var ngRouteModule = angular.module('ngRoute', ['ng']). * ## Dependencies * Requires the {@link ngRoute `ngRoute`} module to be installed. */ -function $RouteProvider(){ +function $RouteProvider() { function inherit(parent, extra) { - return angular.extend(new (angular.extend(function() {}, {prototype:parent}))(), extra); + return angular.extend(Object.create(parent), extra); } var routes = {}; @@ -78,8 +78,8 @@ function $RouteProvider(){ * - `controller` – `{(string|function()=}` – Controller fn that should be associated with * newly created scope or the name of a {@link angular.Module#controller registered * controller} if passed as a string. - * - `controllerAs` – `{string=}` – A controller alias name. If present the controller will be - * published to scope under the `controllerAs` name. + * - `controllerAs` – `{string=}` – An identifier name for a reference to the controller. + * If present, the controller will be published to scope under the `controllerAs` name. * - `template` – `{string=|function()=}` – html template as a string or a function that * returns an html template as a string which should be used by {@link * ngRoute.directive:ngView ngView} or {@link ng.directive:ngInclude ngInclude} directives. @@ -105,8 +105,17 @@ function $RouteProvider(){ * If all the promises are resolved successfully, the values of the resolved promises are * injected and {@link ngRoute.$route#$routeChangeSuccess $routeChangeSuccess} event is * fired. If any of the promises are rejected the - * {@link ngRoute.$route#$routeChangeError $routeChangeError} event is fired. The map object - * is: + * {@link ngRoute.$route#$routeChangeError $routeChangeError} event is fired. + * For easier access to the resolved dependencies from the template, the `resolve` map will + * be available on the scope of the route, under `$resolve` (by default) or a custom name + * specified by the `resolveAs` property (see below). This can be particularly useful, when + * working with {@link angular.Module#component components} as route templates.
        + *
        + * **Note:** If your scope already contains a property with this name, it will be hidden + * or overwritten. Make sure, you specify an appropriate name for this property, that + * does not collide with other properties on the scope. + *
        + * The map object is: * * - `key` – `{string}`: a name of a dependency to be injected into the controller. * - `factory` - `{string|function}`: If `string` then it is an alias for a service. @@ -116,7 +125,10 @@ function $RouteProvider(){ * `ngRoute.$routeParams` will still refer to the previous route within these resolve * functions. Use `$route.current.params` to access the new route parameters, instead. * - * - `redirectTo` – {(string|function())=} – value to update + * - `resolveAs` - `{string=}` - The name under which the `resolve` map will be available on + * the scope of the route. If omitted, defaults to `$resolve`. + * + * - `redirectTo` – `{(string|function())=}` – value to update * {@link ng.$location $location} path with and trigger route redirection. * * If `redirectTo` is a function, it will be called with the following parameters: @@ -129,13 +141,13 @@ function $RouteProvider(){ * The custom `redirectTo` function is expected to return a string which will be used * to update `$location.path()` and `$location.search()`. * - * - `[reloadOnSearch=true]` - {boolean=} - reload route when only `$location.search()` + * - `[reloadOnSearch=true]` - `{boolean=}` - reload route when only `$location.search()` * or `$location.hash()` changes. * * If the option is set to `false` and url in the browser changes, then * `$routeUpdate` event is broadcasted on the root scope. * - * - `[caseInsensitiveMatch=false]` - {boolean=} - match routes without being case sensitive + * - `[caseInsensitiveMatch=false]` - `{boolean=}` - match routes without being case sensitive * * If the option is set to `true`, then the particular route can be matched without being * case sensitive @@ -146,27 +158,45 @@ function $RouteProvider(){ * Adds a new route definition to the `$route` service. */ this.when = function(path, route) { + //copy original route object to preserve params inherited from proto chain + var routeCopy = angular.copy(route); + if (angular.isUndefined(routeCopy.reloadOnSearch)) { + routeCopy.reloadOnSearch = true; + } + if (angular.isUndefined(routeCopy.caseInsensitiveMatch)) { + routeCopy.caseInsensitiveMatch = this.caseInsensitiveMatch; + } routes[path] = angular.extend( - {reloadOnSearch: true}, - route, - path && pathRegExp(path, route) + routeCopy, + path && pathRegExp(path, routeCopy) ); // create redirection for trailing slashes if (path) { - var redirectPath = (path[path.length-1] == '/') - ? path.substr(0, path.length-1) - : path +'/'; + var redirectPath = (path[path.length - 1] === '/') + ? path.substr(0, path.length - 1) + : path + '/'; routes[redirectPath] = angular.extend( {redirectTo: path}, - pathRegExp(redirectPath, route) + pathRegExp(redirectPath, routeCopy) ); } return this; }; + /** + * @ngdoc property + * @name $routeProvider#caseInsensitiveMatch + * @description + * + * A boolean property indicating if routes defined + * using this provider should be matched using a case insensitive + * algorithm. Defaults to `false`. + */ + this.caseInsensitiveMatch = false; + /** * @param path {string} path * @param opts {Object} options @@ -188,9 +218,9 @@ function $RouteProvider(){ path = path .replace(/([().])/g, '\\$1') - .replace(/(\/)?:(\w+)([\?\*])?/g, function(_, slash, key, option){ - var optional = option === '?' ? option : null; - var star = option === '*' ? option : null; + .replace(/(\/)?:(\w+)(\*\?|[\?\*])?/g, function(_, slash, key, option) { + var optional = (option === '?' || option === '*?') ? '?' : null; + var star = (option === '*' || option === '*?') ? '*' : null; keys.push({ name: key, optional: !!optional }); slash = slash || ''; return '' @@ -216,10 +246,14 @@ function $RouteProvider(){ * Sets route definition that will be used on route change when no other route definition * is matched. * - * @param {Object} params Mapping information to be assigned to `$route.current`. + * @param {Object|string} params Mapping information to be assigned to `$route.current`. + * If called with a string, the value maps to `redirectTo`. * @returns {Object} self */ this.otherwise = function(params) { + if (typeof params === 'string') { + params = {redirectTo: params}; + } this.when(null, params); return this; }; @@ -230,10 +264,9 @@ function $RouteProvider(){ '$routeParams', '$q', '$injector', - '$http', - '$templateCache', + '$templateRequest', '$sce', - function($rootScope, $location, $routeParams, $q, $injector, $http, $templateCache, $sce) { + function($rootScope, $location, $routeParams, $q, $injector, $templateRequest, $sce) { /** * @ngdoc service @@ -244,7 +277,7 @@ function $RouteProvider(){ * @property {Object} current Reference to the current route definition. * The route definition contains: * - * - `controller`: The controller constructor as define in route definition. + * - `controller`: The controller constructor as defined in the route definition. * - `locals`: A map of locals which is used by {@link ng.$controller $controller} service for * controller instantiation. The `locals` contain * the resolved values of the `resolve` map. Additionally the `locals` also contain: @@ -252,6 +285,10 @@ function $RouteProvider(){ * - `$scope` - The current route scope. * - `$template` - The current route template HTML. * + * The `locals` will be assigned to the route scope's `$resolve` property. You can override + * the property name, using `resolveAs` in the route definition. See + * {@link ngRoute.$routeProvider $routeProvider} for more info. + * * @property {Object} routes Object with all route configuration Objects as its properties. * * @description @@ -270,9 +307,6 @@ function $RouteProvider(){ * This example shows how changing the URL hash causes the `$route` to match a route against the * URL, and the `ngView` pulls in the partial. * - * Note that this example is using {@link ng.directive:script inlined templates} - * to get it working on jsfiddle as well. - * * * @@ -380,6 +414,10 @@ function $RouteProvider(){ * defined in `resolve` route property. Once all of the dependencies are resolved * `$routeChangeSuccess` is fired. * + * The route change (and the `$location` change that triggered it) can be prevented + * by calling `preventDefault` method of the event. See {@link ng.$rootScope.Scope#$on} + * for more details about event object. + * * @param {Object} angularEvent Synthetic event object. * @param {Route} next Future route information. * @param {Route} current Current route information. @@ -390,7 +428,9 @@ function $RouteProvider(){ * @name $route#$routeChangeSuccess * @eventType broadcast on root scope * @description - * Broadcasted after a route dependencies are resolved. + * Broadcasted after a route change has happened successfully. + * The `resolve` dependencies are now available in the `current.locals` property. + * * {@link ngRoute.directive:ngView ngView} listens for the directive * to instantiate the controller and render the view. * @@ -418,12 +458,16 @@ function $RouteProvider(){ * @name $route#$routeUpdate * @eventType broadcast on root scope * @description - * * The `reloadOnSearch` property has been set to false, and we are reusing the same * instance of the Controller. + * + * @param {Object} angularEvent Synthetic event object + * @param {Route} current Current/previous route information. */ var forceReload = false, + preparedRoute, + preparedRouteIsUpdateOnly, $route = { routes: routes, @@ -436,15 +480,52 @@ function $RouteProvider(){ * {@link ng.$location $location} hasn't changed. * * As a result of that, {@link ngRoute.directive:ngView ngView} - * creates new scope, reinstantiates the controller. + * creates new scope and reinstantiates the controller. */ reload: function() { forceReload = true; - $rootScope.$evalAsync(updateRoute); + + var fakeLocationEvent = { + defaultPrevented: false, + preventDefault: function fakePreventDefault() { + this.defaultPrevented = true; + forceReload = false; + } + }; + + $rootScope.$evalAsync(function() { + prepareRoute(fakeLocationEvent); + if (!fakeLocationEvent.defaultPrevented) commitRoute(); + }); + }, + + /** + * @ngdoc method + * @name $route#updateParams + * + * @description + * Causes `$route` service to update the current URL, replacing + * current route parameters with those specified in `newParams`. + * Provided property names that match the route's path segment + * definitions will be interpolated into the location's path, while + * remaining properties will be treated as query params. + * + * @param {!Object} newParams mapping of URL parameter names to values + */ + updateParams: function(newParams) { + if (this.current && this.current.$$route) { + newParams = angular.extend({}, this.current.params, newParams); + $location.path(interpolate(this.current.$$route.originalPath, newParams)); + // interpolate modifies newParams, only query params are left + $location.search(newParams); + } else { + throw $routeMinErr('norout', 'Tried updating route when with no current route'); + } } }; - $rootScope.$on('$locationChangeSuccess', updateRoute); + $rootScope.$on('$locationChangeStart', prepareRoute); + $rootScope.$on('$locationChangeSuccess', commitRoute); return $route; @@ -482,56 +563,68 @@ function $RouteProvider(){ return params; } - function updateRoute() { - var next = parseRoute(), - last = $route.current; + function prepareRoute($locationEvent) { + var lastRoute = $route.current; - if (next && last && next.$$route === last.$$route - && angular.equals(next.pathParams, last.pathParams) - && !next.reloadOnSearch && !forceReload) { - last.params = next.params; - angular.copy(last.params, $routeParams); - $rootScope.$broadcast('$routeUpdate', last); - } else if (next || last) { + preparedRoute = parseRoute(); + preparedRouteIsUpdateOnly = preparedRoute && lastRoute && preparedRoute.$$route === lastRoute.$$route + && angular.equals(preparedRoute.pathParams, lastRoute.pathParams) + && !preparedRoute.reloadOnSearch && !forceReload; + + if (!preparedRouteIsUpdateOnly && (lastRoute || preparedRoute)) { + if ($rootScope.$broadcast('$routeChangeStart', preparedRoute, lastRoute).defaultPrevented) { + if ($locationEvent) { + $locationEvent.preventDefault(); + } + } + } + } + + function commitRoute() { + var lastRoute = $route.current; + var nextRoute = preparedRoute; + + if (preparedRouteIsUpdateOnly) { + lastRoute.params = nextRoute.params; + angular.copy(lastRoute.params, $routeParams); + $rootScope.$broadcast('$routeUpdate', lastRoute); + } else if (nextRoute || lastRoute) { forceReload = false; - $rootScope.$broadcast('$routeChangeStart', next, last); - $route.current = next; - if (next) { - if (next.redirectTo) { - if (angular.isString(next.redirectTo)) { - $location.path(interpolate(next.redirectTo, next.params)).search(next.params) + $route.current = nextRoute; + if (nextRoute) { + if (nextRoute.redirectTo) { + if (angular.isString(nextRoute.redirectTo)) { + $location.path(interpolate(nextRoute.redirectTo, nextRoute.params)).search(nextRoute.params) .replace(); } else { - $location.url(next.redirectTo(next.pathParams, $location.path(), $location.search())) + $location.url(nextRoute.redirectTo(nextRoute.pathParams, $location.path(), $location.search())) .replace(); } } } - $q.when(next). + $q.when(nextRoute). then(function() { - if (next) { - var locals = angular.extend({}, next.resolve), + if (nextRoute) { + var locals = angular.extend({}, nextRoute.resolve), template, templateUrl; angular.forEach(locals, function(value, key) { locals[key] = angular.isString(value) ? - $injector.get(value) : $injector.invoke(value); + $injector.get(value) : $injector.invoke(value, null, null, key); }); - if (angular.isDefined(template = next.template)) { + if (angular.isDefined(template = nextRoute.template)) { if (angular.isFunction(template)) { - template = template(next.params); + template = template(nextRoute.params); } - } else if (angular.isDefined(templateUrl = next.templateUrl)) { + } else if (angular.isDefined(templateUrl = nextRoute.templateUrl)) { if (angular.isFunction(templateUrl)) { - templateUrl = templateUrl(next.params); + templateUrl = templateUrl(nextRoute.params); } - templateUrl = $sce.getTrustedResourceUrl(templateUrl); if (angular.isDefined(templateUrl)) { - next.loadedTemplateUrl = templateUrl; - template = $http.get(templateUrl, {cache: $templateCache}). - then(function(response) { return response.data; }); + nextRoute.loadedTemplateUrl = $sce.valueOf(templateUrl); + template = $templateRequest(templateUrl); } } if (angular.isDefined(template)) { @@ -540,18 +633,18 @@ function $RouteProvider(){ return $q.all(locals); } }). - // after route change then(function(locals) { - if (next == $route.current) { - if (next) { - next.locals = locals; - angular.copy(next.params, $routeParams); + // after route change + if (nextRoute === $route.current) { + if (nextRoute) { + nextRoute.locals = locals; + angular.copy(nextRoute.params, $routeParams); } - $rootScope.$broadcast('$routeChangeSuccess', next, last); + $rootScope.$broadcast('$routeChangeSuccess', nextRoute, lastRoute); } }, function(error) { - if (next == $route.current) { - $rootScope.$broadcast('$routeChangeError', next, last, error); + if (nextRoute === $route.current) { + $rootScope.$broadcast('$routeChangeError', nextRoute, lastRoute, error); } }); } @@ -581,11 +674,11 @@ function $RouteProvider(){ */ function interpolate(string, params) { var result = []; - angular.forEach((string||'').split(':'), function(segment, i) { + angular.forEach((string || '').split(':'), function(segment, i) { if (i === 0) { result.push(segment); } else { - var segmentMatch = segment.match(/(\w+)(.*)/); + var segmentMatch = segment.match(/(\w+)(?:[?*])?(.*)/); var key = segmentMatch[1]; result.push(params[key]); result.push(segmentMatch[2] || ''); @@ -656,11 +749,20 @@ ngRouteModule.directive('ngView', ngViewFillContentFactory); * Requires the {@link ngRoute `ngRoute`} module to be installed. * * @animations - * enter - animation is used to bring new content into the browser. - * leave - animation is used to animate existing content away. + * | Animation | Occurs | + * |----------------------------------|-------------------------------------| + * | {@link ng.$animate#enter enter} | when the new element is inserted to the DOM | + * | {@link ng.$animate#leave leave} | when the old element is removed from to the DOM | * * The enter and leave animation occur concurrently. * + * @knownIssue If `ngView` is contained in an asynchronously loaded template (e.g. in another + * directive's templateUrl or in a template loaded using `ngInclude`), then you need to + * make sure that `$route` is instantiated in time to capture the initial + * `$locationChangeStart` event and load the appropriate view. One way to achieve this + * is to have it as a dependency in a `.run` block: + * `myModule.run(['$route', function() {}]);` + * * @scope * @priority 400 * @param {string=} onload Expression to evaluate whenever the view updates. @@ -693,7 +795,6 @@ ngRouteModule.directive('ngView', ngViewFillContentFactory);
        $location.path() = {{main.$location.path()}}
        $route.current.templateUrl = {{main.$route.current.templateUrl}}
        $route.current.params = {{main.$route.current.params}}
        -
        $route.current.scope.name = {{main.$route.current.scope.name}}
        $routeParams = {{main.$routeParams}}
        @@ -717,7 +818,6 @@ ngRouteModule.directive('ngView', ngViewFillContentFactory); .view-animate-container { position:relative; height:100px!important; - position:relative; background:white; border:1px solid black; height:40px; @@ -729,7 +829,6 @@ ngRouteModule.directive('ngView', ngViewFillContentFactory); } .view-animate.ng-enter, .view-animate.ng-leave { - -webkit-transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 1.5s; transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 1.5s; display:block; @@ -771,7 +870,6 @@ ngRouteModule.directive('ngView', ngViewFillContentFactory); controllerAs: 'chapter' }); - // configure html5 to get links working on jsfiddle $locationProvider.html5Mode(true); }]) .controller('MainCtrl', ['$route', '$routeParams', '$location', @@ -818,7 +916,7 @@ ngRouteModule.directive('ngView', ngViewFillContentFactory); * Emitted every time the ngView content is reloaded. */ ngViewFactory.$inject = ['$route', '$anchorScroll', '$animate']; -function ngViewFactory( $route, $anchorScroll, $animate) { +function ngViewFactory($route, $anchorScroll, $animate) { return { restrict: 'ECA', terminal: true, @@ -827,7 +925,7 @@ function ngViewFactory( $route, $anchorScroll, $animate) { link: function(scope, $element, attr, ctrl, $transclude) { var currentScope, currentElement, - previousElement, + previousLeaveAnimation, autoScrollExp = attr.autoscroll, onloadExp = attr.onload || ''; @@ -835,19 +933,20 @@ function ngViewFactory( $route, $anchorScroll, $animate) { update(); function cleanupLastView() { - if(previousElement) { - previousElement.remove(); - previousElement = null; + if (previousLeaveAnimation) { + $animate.cancel(previousLeaveAnimation); + previousLeaveAnimation = null; } - if(currentScope) { + + if (currentScope) { currentScope.$destroy(); currentScope = null; } - if(currentElement) { - $animate.leave(currentElement, function() { - previousElement = null; + if (currentElement) { + previousLeaveAnimation = $animate.leave(currentElement); + previousLeaveAnimation.then(function() { + previousLeaveAnimation = null; }); - previousElement = currentElement; currentElement = null; } } @@ -867,7 +966,7 @@ function ngViewFactory( $route, $anchorScroll, $animate) { // function is called before linking the content, which would apply child // directives to non existing elements. var clone = $transclude(newScope, function(clone) { - $animate.enter(clone, null, currentElement || $element, function onNgViewEnter () { + $animate.enter(clone, null, currentElement || $element).then(function onNgViewEnter() { if (angular.isDefined(autoScrollExp) && (!autoScrollExp || scope.$eval(autoScrollExp))) { $anchorScroll(); @@ -915,6 +1014,7 @@ function ngViewFillContentFactory($compile, $controller, $route) { $element.data('$ngControllerController', controller); $element.children().data('$ngControllerController', controller); } + scope[current.resolveAs || '$resolve'] = locals; link(scope); } diff --git a/app/bower_components/angular-route/angular-route.min.js b/app/bower_components/angular-route/angular-route.min.js index 54e7dc9..4dcf7b4 100644 --- a/app/bower_components/angular-route/angular-route.min.js +++ b/app/bower_components/angular-route/angular-route.min.js @@ -1,14 +1,15 @@ /* - AngularJS v1.2.21 - (c) 2010-2014 Google, Inc. http://angularjs.org + AngularJS v1.5.6-build.4801+sha.3b5751d + (c) 2010-2016 Google, Inc. http://angularjs.org License: MIT */ -(function(n,e,A){'use strict';function x(s,g,h){return{restrict:"ECA",terminal:!0,priority:400,transclude:"element",link:function(a,c,b,f,w){function y(){p&&(p.remove(),p=null);k&&(k.$destroy(),k=null);l&&(h.leave(l,function(){p=null}),p=l,l=null)}function v(){var b=s.current&&s.current.locals;if(e.isDefined(b&&b.$template)){var b=a.$new(),d=s.current;l=w(b,function(d){h.enter(d,null,l||c,function(){!e.isDefined(t)||t&&!a.$eval(t)||g()});y()});k=d.scope=b;k.$emit("$viewContentLoaded");k.$eval(u)}else y()} -var k,l,p,t=b.autoscroll,u=b.onload||"";a.$on("$routeChangeSuccess",v);v()}}}function z(e,g,h){return{restrict:"ECA",priority:-400,link:function(a,c){var b=h.current,f=b.locals;c.html(f.$template);var w=e(c.contents());b.controller&&(f.$scope=a,f=g(b.controller,f),b.controllerAs&&(a[b.controllerAs]=f),c.data("$ngControllerController",f),c.children().data("$ngControllerController",f));w(a)}}}n=e.module("ngRoute",["ng"]).provider("$route",function(){function s(a,c){return e.extend(new (e.extend(function(){}, -{prototype:a})),c)}function g(a,e){var b=e.caseInsensitiveMatch,f={originalPath:a,regexp:a},h=f.keys=[];a=a.replace(/([().])/g,"\\$1").replace(/(\/)?:(\w+)([\?\*])?/g,function(a,e,b,c){a="?"===c?c:null;c="*"===c?c:null;h.push({name:b,optional:!!a});e=e||"";return""+(a?"":e)+"(?:"+(a?e:"")+(c&&"(.+?)"||"([^/]+)")+(a||"")+")"+(a||"")}).replace(/([\/$\*])/g,"\\$1");f.regexp=RegExp("^"+a+"$",b?"i":"");return f}var h={};this.when=function(a,c){h[a]=e.extend({reloadOnSearch:!0},c,a&&g(a,c));if(a){var b= -"/"==a[a.length-1]?a.substr(0,a.length-1):a+"/";h[b]=e.extend({redirectTo:a},g(b,c))}return this};this.otherwise=function(a){this.when(null,a);return this};this.$get=["$rootScope","$location","$routeParams","$q","$injector","$http","$templateCache","$sce",function(a,c,b,f,g,n,v,k){function l(){var d=p(),m=r.current;if(d&&m&&d.$$route===m.$$route&&e.equals(d.pathParams,m.pathParams)&&!d.reloadOnSearch&&!u)m.params=d.params,e.copy(m.params,b),a.$broadcast("$routeUpdate",m);else if(d||m)u=!1,a.$broadcast("$routeChangeStart", -d,m),(r.current=d)&&d.redirectTo&&(e.isString(d.redirectTo)?c.path(t(d.redirectTo,d.params)).search(d.params).replace():c.url(d.redirectTo(d.pathParams,c.path(),c.search())).replace()),f.when(d).then(function(){if(d){var a=e.extend({},d.resolve),c,b;e.forEach(a,function(d,c){a[c]=e.isString(d)?g.get(d):g.invoke(d)});e.isDefined(c=d.template)?e.isFunction(c)&&(c=c(d.params)):e.isDefined(b=d.templateUrl)&&(e.isFunction(b)&&(b=b(d.params)),b=k.getTrustedResourceUrl(b),e.isDefined(b)&&(d.loadedTemplateUrl= -b,c=n.get(b,{cache:v}).then(function(a){return a.data})));e.isDefined(c)&&(a.$template=c);return f.all(a)}}).then(function(c){d==r.current&&(d&&(d.locals=c,e.copy(d.params,b)),a.$broadcast("$routeChangeSuccess",d,m))},function(c){d==r.current&&a.$broadcast("$routeChangeError",d,m,c)})}function p(){var a,b;e.forEach(h,function(f,h){var q;if(q=!b){var g=c.path();q=f.keys;var l={};if(f.regexp)if(g=f.regexp.exec(g)){for(var k=1,p=g.length;k", + "license": "MIT", + "bugs": { + "url": "https://github.com/angular/angular.js/issues" + }, + "homepage": "http://angularjs.org" +} diff --git a/app/bower_components/angular-sanitize/.bower.json b/app/bower_components/angular-sanitize/.bower.json index 59d6158..59cd7ff 100644 --- a/app/bower_components/angular-sanitize/.bower.json +++ b/app/bower_components/angular-sanitize/.bower.json @@ -1,18 +1,20 @@ { "name": "angular-sanitize", - "version": "1.2.21", + "version": "1.5.6-build.4801+sha.3b5751d", + "license": "MIT", "main": "./angular-sanitize.js", + "ignore": [], "dependencies": { - "angular": "1.2.21" + "angular": "1.5.6-build.4801+sha.3b5751d" }, "homepage": "https://github.com/angular/bower-angular-sanitize", - "_release": "1.2.21", + "_release": "1.5.6-build.4801+sha.3b5751d", "_resolution": { "type": "version", - "tag": "v1.2.21", - "commit": "19b66fdaa4970230ed1aeb4a8a80ed41a8bced12" + "tag": "v1.5.6-build.4801+sha.3b5751d", + "commit": "4fb1e8495181f0390414a6ab4f0a0a32ec0b5698" }, - "_source": "git://github.com/angular/bower-angular-sanitize.git", - "_target": "~1.2.21", + "_source": "https://github.com/angular/bower-angular-sanitize.git", + "_target": "~1.5.6", "_originalSource": "angular-sanitize" } \ No newline at end of file diff --git a/app/bower_components/angular-sanitize/README.md b/app/bower_components/angular-sanitize/README.md index 585a235..b84aaf6 100644 --- a/app/bower_components/angular-sanitize/README.md +++ b/app/bower_components/angular-sanitize/README.md @@ -1,12 +1,26 @@ -# bower-angular-sanitize +# packaged angular-sanitize -This repo is for distribution on `bower`. The source for this module is in the +This repo is for distribution on `npm` and `bower`. The source for this module is in the [main AngularJS repo](https://github.com/angular/angular.js/tree/master/src/ngSanitize). Please file issues and pull requests against that repo. ## Install -Install with `bower`: +You can install this package either with `npm` or with `bower`. + +### npm + +```shell +npm install angular-sanitize +``` + +Then add `ngSanitize` as a dependency for your app: + +```javascript +angular.module('myApp', [require('angular-sanitize')]); +``` + +### bower ```shell bower install angular-sanitize @@ -18,7 +32,7 @@ Add a ` ``` -And add `ngSanitize` as a dependency for your app: +Then add `ngSanitize` as a dependency for your app: ```javascript angular.module('myApp', ['ngSanitize']); @@ -33,7 +47,7 @@ Documentation is available on the The MIT License -Copyright (c) 2010-2012 Google, Inc. http://angularjs.org +Copyright (c) 2010-2015 Google, Inc. http://angularjs.org Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/app/bower_components/angular-sanitize/angular-sanitize.js b/app/bower_components/angular-sanitize/angular-sanitize.js index 4a489f3..0d71a71 100644 --- a/app/bower_components/angular-sanitize/angular-sanitize.js +++ b/app/bower_components/angular-sanitize/angular-sanitize.js @@ -1,9 +1,20 @@ /** - * @license AngularJS v1.2.21 - * (c) 2010-2014 Google, Inc. http://angularjs.org + * @license AngularJS v1.5.6-build.4801+sha.3b5751d + * (c) 2010-2016 Google, Inc. http://angularjs.org * License: MIT */ -(function(window, angular, undefined) {'use strict'; +(function(window, angular) {'use strict'; + +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * Any commits to this file should be reviewed with security in mind. * + * Changes to this file can potentially create security vulnerabilities. * + * An approval from 2 Core members with history of modifying * + * this file is required. * + * * + * Does the change somehow allow for arbitrary javascript to be executed? * + * Or allows for someone to change the prototype of built-in objects? * + * Or gives undesired access to variables likes document or window? * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ var $sanitizeMinErr = angular.$$minErr('$sanitize'); @@ -22,39 +33,26 @@ var $sanitizeMinErr = angular.$$minErr('$sanitize'); * See {@link ngSanitize.$sanitize `$sanitize`} for usage. */ -/* - * HTML Parser By Misko Hevery (misko@hevery.com) - * based on: HTML Parser By John Resig (ejohn.org) - * Original code by Erik Arvidsson, Mozilla Public License - * http://erik.eae.net/simplehtmlparser/simplehtmlparser.js - * - * // Use like so: - * htmlParser(htmlString, { - * start: function(tag, attrs, unary) {}, - * end: function(tag) {}, - * chars: function(text) {}, - * comment: function(text) {} - * }); - * - */ - - /** * @ngdoc service * @name $sanitize * @kind function * * @description - * The input is sanitized by parsing the html into tokens. All safe tokens (from a whitelist) are - * then serialized back to properly escaped html string. This means that no unsafe input can make - * it into the returned string, however, since our parser is more strict than a typical browser - * parser, it's possible that some obscure input, which would be recognized as valid HTML by a - * browser, won't make it through the sanitizer. - * The whitelist is configured using the functions `aHrefSanitizationWhitelist` and - * `imgSrcSanitizationWhitelist` of {@link ng.$compileProvider `$compileProvider`}. + * Sanitizes an html string by stripping all potentially dangerous tokens. * - * @param {string} html Html input. - * @returns {string} Sanitized html. + * The input is sanitized by parsing the HTML into tokens. All safe tokens (from a whitelist) are + * then serialized back to properly escaped html string. This means that no unsafe input can make + * it into the returned string. + * + * The whitelist for URL sanitization of attribute values is configured using the functions + * `aHrefSanitizationWhitelist` and `imgSrcSanitizationWhitelist` of {@link ng.$compileProvider + * `$compileProvider`}. + * + * The input may also contain SVG markup if this is enabled via {@link $sanitizeProvider}. + * + * @param {string} html HTML input. + * @returns {string} Sanitized HTML. * * @example @@ -137,16 +135,70 @@ var $sanitizeMinErr = angular.$$minErr('$sanitize'); */ + + +/** + * @ngdoc provider + * @name $sanitizeProvider + * + * @description + * Creates and configures {@link $sanitize} instance. + */ function $SanitizeProvider() { + var svgEnabled = false; + this.$get = ['$$sanitizeUri', function($$sanitizeUri) { + if (svgEnabled) { + angular.extend(validElements, svgElements); + } return function(html) { var buf = []; htmlParser(html, htmlSanitizeWriter(buf, function(uri, isImage) { - return !/^unsafe/.test($$sanitizeUri(uri, isImage)); + return !/^unsafe:/.test($$sanitizeUri(uri, isImage)); })); return buf.join(''); }; }]; + + + /** + * @ngdoc method + * @name $sanitizeProvider#enableSvg + * @kind function + * + * @description + * Enables a subset of svg to be supported by the sanitizer. + * + *
        + *

        By enabling this setting without taking other precautions, you might expose your + * application to click-hijacking attacks. In these attacks, sanitized svg elements could be positioned + * outside of the containing element and be rendered over other elements on the page (e.g. a login + * link). Such behavior can then result in phishing incidents.

        + * + *

        To protect against these, explicitly setup `overflow: hidden` css rule for all potential svg + * tags within the sanitized content:

        + * + *
        + * + *
        
        +   *   .rootOfTheIncludedContent svg {
        +   *     overflow: hidden !important;
        +   *   }
        +   *   
        + *
        + * + * @param {boolean=} flag Enable or disable SVG support in the sanitizer. + * @returns {boolean|ng.$sanitizeProvider} Returns the currently configured value if called + * without an argument or self for chaining otherwise. + */ + this.enableSvg = function(enableSvg) { + if (angular.isDefined(enableSvg)) { + svgEnabled = enableSvg; + return this; + } else { + return svgEnabled; + } + }; } function sanitizeText(chars) { @@ -158,18 +210,9 @@ function sanitizeText(chars) { // Regular Expressions for parsing tags and attributes -var START_TAG_REGEXP = - /^<((?:[a-zA-Z])[\w:-]*)((?:\s+[\w:-]+(?:\s*=\s*(?:(?:"[^"]*")|(?:'[^']*')|[^>\s]+))?)*)\s*(\/?)\s*(>?)/, - END_TAG_REGEXP = /^<\/\s*([\w:-]+)[^>]*>/, - ATTR_REGEXP = /([\w:-]+)(?:\s*=\s*(?:(?:"((?:[^"])*)")|(?:'((?:[^'])*)')|([^>\s]+)))?/g, - BEGIN_TAG_REGEXP = /^/g, - DOCTYPE_REGEXP = /]*?)>/i, - CDATA_REGEXP = //g, - SURROGATE_PAIR_REGEXP = /[\uD800-\uDBFF][\uDC00-\uDFFF]/g, +var SURROGATE_PAIR_REGEXP = /[\uD800-\uDBFF][\uDC00-\uDFFF]/g, // Match everything outside of normal chars and " (quote character) - NON_ALPHANUMERIC_REGEXP = /([^\#-~| |!])/g; + NON_ALPHANUMERIC_REGEXP = /([^\#-~ |!])/g; // Good source of info about elements and attributes @@ -178,29 +221,36 @@ var START_TAG_REGEXP = // Safe Void Elements - HTML5 // http://dev.w3.org/html5/spec/Overview.html#void-elements -var voidElements = makeMap("area,br,col,hr,img,wbr"); +var voidElements = toMap("area,br,col,hr,img,wbr"); // Elements that you can, intentionally, leave open (and which close themselves) // http://dev.w3.org/html5/spec/Overview.html#optional-tags -var optionalEndTagBlockElements = makeMap("colgroup,dd,dt,li,p,tbody,td,tfoot,th,thead,tr"), - optionalEndTagInlineElements = makeMap("rp,rt"), +var optionalEndTagBlockElements = toMap("colgroup,dd,dt,li,p,tbody,td,tfoot,th,thead,tr"), + optionalEndTagInlineElements = toMap("rp,rt"), optionalEndTagElements = angular.extend({}, optionalEndTagInlineElements, optionalEndTagBlockElements); // Safe Block Elements - HTML5 -var blockElements = angular.extend({}, optionalEndTagBlockElements, makeMap("address,article," + +var blockElements = angular.extend({}, optionalEndTagBlockElements, toMap("address,article," + "aside,blockquote,caption,center,del,dir,div,dl,figure,figcaption,footer,h1,h2,h3,h4,h5," + - "h6,header,hgroup,hr,ins,map,menu,nav,ol,pre,script,section,table,ul")); + "h6,header,hgroup,hr,ins,map,menu,nav,ol,pre,section,table,ul")); // Inline Elements - HTML5 -var inlineElements = angular.extend({}, optionalEndTagInlineElements, makeMap("a,abbr,acronym,b," + +var inlineElements = angular.extend({}, optionalEndTagInlineElements, toMap("a,abbr,acronym,b," + "bdi,bdo,big,br,cite,code,del,dfn,em,font,i,img,ins,kbd,label,map,mark,q,ruby,rp,rt,s," + "samp,small,span,strike,strong,sub,sup,time,tt,u,var")); +// SVG Elements +// https://wiki.whatwg.org/wiki/Sanitization_rules#svg_Elements +// Note: the elements animate,animateColor,animateMotion,animateTransform,set are intentionally omitted. +// They can potentially allow for arbitrary javascript to be executed. See #11290 +var svgElements = toMap("circle,defs,desc,ellipse,font-face,font-face-name,font-face-src,g,glyph," + + "hkern,image,linearGradient,line,marker,metadata,missing-glyph,mpath,path,polygon,polyline," + + "radialGradient,rect,stop,svg,switch,text,title,tspan"); -// Special Elements (can contain anything) -var specialElements = makeMap("script,style"); +// Blocked Elements (will be stripped) +var blockedElements = toMap("script,style"); var validElements = angular.extend({}, voidElements, @@ -209,25 +259,71 @@ var validElements = angular.extend({}, optionalEndTagElements); //Attributes that have href and hence need to be sanitized -var uriAttrs = makeMap("background,cite,href,longdesc,src,usemap"); -var validAttrs = angular.extend({}, uriAttrs, makeMap( - 'abbr,align,alt,axis,bgcolor,border,cellpadding,cellspacing,class,clear,'+ - 'color,cols,colspan,compact,coords,dir,face,headers,height,hreflang,hspace,'+ - 'ismap,lang,language,nohref,nowrap,rel,rev,rows,rowspan,rules,'+ - 'scope,scrolling,shape,size,span,start,summary,target,title,type,'+ - 'valign,value,vspace,width')); +var uriAttrs = toMap("background,cite,href,longdesc,src,xlink:href"); -function makeMap(str) { +var htmlAttrs = toMap('abbr,align,alt,axis,bgcolor,border,cellpadding,cellspacing,class,clear,' + + 'color,cols,colspan,compact,coords,dir,face,headers,height,hreflang,hspace,' + + 'ismap,lang,language,nohref,nowrap,rel,rev,rows,rowspan,rules,' + + 'scope,scrolling,shape,size,span,start,summary,tabindex,target,title,type,' + + 'valign,value,vspace,width'); + +// SVG attributes (without "id" and "name" attributes) +// https://wiki.whatwg.org/wiki/Sanitization_rules#svg_Attributes +var svgAttrs = toMap('accent-height,accumulate,additive,alphabetic,arabic-form,ascent,' + + 'baseProfile,bbox,begin,by,calcMode,cap-height,class,color,color-rendering,content,' + + 'cx,cy,d,dx,dy,descent,display,dur,end,fill,fill-rule,font-family,font-size,font-stretch,' + + 'font-style,font-variant,font-weight,from,fx,fy,g1,g2,glyph-name,gradientUnits,hanging,' + + 'height,horiz-adv-x,horiz-origin-x,ideographic,k,keyPoints,keySplines,keyTimes,lang,' + + 'marker-end,marker-mid,marker-start,markerHeight,markerUnits,markerWidth,mathematical,' + + 'max,min,offset,opacity,orient,origin,overline-position,overline-thickness,panose-1,' + + 'path,pathLength,points,preserveAspectRatio,r,refX,refY,repeatCount,repeatDur,' + + 'requiredExtensions,requiredFeatures,restart,rotate,rx,ry,slope,stemh,stemv,stop-color,' + + 'stop-opacity,strikethrough-position,strikethrough-thickness,stroke,stroke-dasharray,' + + 'stroke-dashoffset,stroke-linecap,stroke-linejoin,stroke-miterlimit,stroke-opacity,' + + 'stroke-width,systemLanguage,target,text-anchor,to,transform,type,u1,u2,underline-position,' + + 'underline-thickness,unicode,unicode-range,units-per-em,values,version,viewBox,visibility,' + + 'width,widths,x,x-height,x1,x2,xlink:actuate,xlink:arcrole,xlink:role,xlink:show,xlink:title,' + + 'xlink:type,xml:base,xml:lang,xml:space,xmlns,xmlns:xlink,y,y1,y2,zoomAndPan', true); + +var validAttrs = angular.extend({}, + uriAttrs, + svgAttrs, + htmlAttrs); + +function toMap(str, lowercaseKeys) { var obj = {}, items = str.split(','), i; - for (i = 0; i < items.length; i++) obj[items[i]] = true; + for (i = 0; i < items.length; i++) { + obj[lowercaseKeys ? angular.lowercase(items[i]) : items[i]] = true; + } return obj; } +var inertBodyElement; +(function(window) { + var doc; + if (window.document && window.document.implementation) { + doc = window.document.implementation.createHTMLDocument("inert"); + } else { + throw $sanitizeMinErr('noinert', "Can't create an inert html document"); + } + var docElement = doc.documentElement || doc.getDocumentElement(); + var bodyElements = docElement.getElementsByTagName('body'); + + // usually there should be only one body element in the document, but IE doesn't have any, so we need to create one + if (bodyElements.length === 1) { + inertBodyElement = bodyElements[0]; + } else { + var html = doc.createElement('html'); + inertBodyElement = doc.createElement('body'); + html.appendChild(inertBodyElement); + doc.appendChild(html); + } +})(window); /** * @example * htmlParser(htmlString, { - * start: function(tag, attrs, unary) {}, + * start: function(tag, attrs) {}, * end: function(tag) {}, * chars: function(text) {}, * comment: function(text) {} @@ -236,174 +332,76 @@ function makeMap(str) { * @param {string} html string * @param {object} handler */ -function htmlParser( html, handler ) { - var index, chars, match, stack = [], last = html, text; - stack.last = function() { return stack[ stack.length - 1 ]; }; +function htmlParser(html, handler) { + if (html === null || html === undefined) { + html = ''; + } else if (typeof html !== 'string') { + html = '' + html; + } + inertBodyElement.innerHTML = html; - while ( html ) { - text = ''; - chars = true; + //mXSS protection + var mXSSAttempts = 5; + do { + if (mXSSAttempts === 0) { + throw $sanitizeMinErr('uinput', "Failed to sanitize html because the input is unstable"); + } + mXSSAttempts--; - // Make sure we're not in a script or style element - if ( !stack.last() || !specialElements[ stack.last() ] ) { + // strip custom-namespaced attributes on IE<=11 + if (window.document.documentMode) { + stripCustomNsAttrs(inertBodyElement); + } + html = inertBodyElement.innerHTML; //trigger mXSS + inertBodyElement.innerHTML = html; + } while (html !== inertBodyElement.innerHTML); - // Comment - if ( html.indexOf("", index) === index) { - if (handler.comment) handler.comment( html.substring( 4, index ) ); - html = html.substring( index + 3 ); - chars = false; - } - // DOCTYPE - } else if ( DOCTYPE_REGEXP.test(html) ) { - match = html.match( DOCTYPE_REGEXP ); - - if ( match ) { - html = html.replace( match[0], ''); - chars = false; - } - // end tag - } else if ( BEGING_END_TAGE_REGEXP.test(html) ) { - match = html.match( END_TAG_REGEXP ); - - if ( match ) { - html = html.substring( match[0].length ); - match[0].replace( END_TAG_REGEXP, parseEndTag ); - chars = false; - } - - // start tag - } else if ( BEGIN_TAG_REGEXP.test(html) ) { - match = html.match( START_TAG_REGEXP ); - - if ( match ) { - // We only have a valid start-tag if there is a '>'. - if ( match[4] ) { - html = html.substring( match[0].length ); - match[0].replace( START_TAG_REGEXP, parseStartTag ); + var nextNode; + if (!(nextNode = node.firstChild)) { + if (node.nodeType === 1) { + handler.end(node.nodeName.toLowerCase()); + } + nextNode = node.nextSibling; + if (!nextNode) { + while (nextNode == null) { + node = node.parentNode; + if (node === inertBodyElement) break; + nextNode = node.nextSibling; + if (node.nodeType === 1) { + handler.end(node.nodeName.toLowerCase()); } - chars = false; - } else { - // no ending tag found --- this piece should be encoded as an entity. - text += '<'; - html = html.substring(1); } } - - if ( chars ) { - index = html.indexOf("<"); - - text += index < 0 ? html : html.substring( 0, index ); - html = index < 0 ? "" : html.substring( index ); - - if (handler.chars) handler.chars( decodeEntities(text) ); - } - - } else { - html = html.replace(new RegExp("(.*)<\\s*\\/\\s*" + stack.last() + "[^>]*>", 'i'), - function(all, text){ - text = text.replace(COMMENT_REGEXP, "$1").replace(CDATA_REGEXP, "$1"); - - if (handler.chars) handler.chars( decodeEntities(text) ); - - return ""; - }); - - parseEndTag( "", stack.last() ); } - - if ( html == last ) { - throw $sanitizeMinErr('badparse', "The sanitizer was unable to parse the following block " + - "of html: {0}", html); - } - last = html; + node = nextNode; } - // Clean up any remaining tags - parseEndTag(); - - function parseStartTag( tag, tagName, rest, unary ) { - tagName = angular.lowercase(tagName); - if ( blockElements[ tagName ] ) { - while ( stack.last() && inlineElements[ stack.last() ] ) { - parseEndTag( "", stack.last() ); - } - } - - if ( optionalEndTagElements[ tagName ] && stack.last() == tagName ) { - parseEndTag( "", tagName ); - } - - unary = voidElements[ tagName ] || !!unary; - - if ( !unary ) - stack.push( tagName ); - - var attrs = {}; - - rest.replace(ATTR_REGEXP, - function(match, name, doubleQuotedValue, singleQuotedValue, unquotedValue) { - var value = doubleQuotedValue - || singleQuotedValue - || unquotedValue - || ''; - - attrs[name] = decodeEntities(value); - }); - if (handler.start) handler.start( tagName, attrs, unary ); - } - - function parseEndTag( tag, tagName ) { - var pos = 0, i; - tagName = angular.lowercase(tagName); - if ( tagName ) - // Find the closest opened tag of the same type - for ( pos = stack.length - 1; pos >= 0; pos-- ) - if ( stack[ pos ] == tagName ) - break; - - if ( pos >= 0 ) { - // Close all the open elements, up the stack - for ( i = stack.length - 1; i >= pos; i-- ) - if (handler.end) handler.end( stack[ i ] ); - - // Remove the open elements from the stack - stack.length = pos; - } + while (node = inertBodyElement.firstChild) { + inertBodyElement.removeChild(node); } } -var hiddenPre=document.createElement("pre"); -var spaceRe = /^(\s*)([\s\S]*?)(\s*)$/; -/** - * decodes all entities into regular string - * @param value - * @returns {string} A string with decoded entities. - */ -function decodeEntities(value) { - if (!value) { return ''; } - - // Note: IE8 does not preserve spaces at the start/end of innerHTML - // so we must capture them and reattach them afterward - var parts = spaceRe.exec(value); - var spaceBefore = parts[1]; - var spaceAfter = parts[3]; - var content = parts[2]; - if (content) { - hiddenPre.innerHTML=content.replace(/' : '>'); + out('>'); } }, - end: function(tag){ - tag = angular.lowercase(tag); - if (!ignore && validElements[tag] === true) { - out(''); - } - if (tag == ignore) { - ignore = false; - } - }, - chars: function(chars){ - if (!ignore) { - out(encodeEntities(chars)); - } + end: function(tag) { + tag = angular.lowercase(tag); + if (!ignoreCurrentElement && validElements[tag] === true && voidElements[tag] !== true) { + out(''); } + if (tag == ignoreCurrentElement) { + ignoreCurrentElement = false; + } + }, + chars: function(chars) { + if (!ignoreCurrentElement) { + out(encodeEntities(chars)); + } + } }; } +/** + * When IE9-11 comes across an unknown namespaced attribute e.g. 'xlink:foo' it adds 'xmlns:ns1' attribute to declare + * ns1 namespace and prefixes the attribute with 'ns1' (e.g. 'ns1:xlink:foo'). This is undesirable since we don't want + * to allow any of these custom attributes. This method strips them all. + * + * @param node Root element to process + */ +function stripCustomNsAttrs(node) { + if (node.nodeType === window.Node.ELEMENT_NODE) { + var attrs = node.attributes; + for (var i = 0, l = attrs.length; i < l; i++) { + var attrNode = attrs[i]; + var attrName = attrNode.name.toLowerCase(); + if (attrName === 'xmlns:ns1' || attrName.indexOf('ns1:') === 0) { + node.removeAttributeNode(attrNode); + i--; + l--; + } + } + } + + var nextNode = node.firstChild; + if (nextNode) { + stripCustomNsAttrs(nextNode); + } + + nextNode = node.nextSibling; + if (nextNode) { + stripCustomNsAttrs(nextNode); + } +} + + + // define ngSanitize module and register $sanitize service angular.module('ngSanitize', []).provider('$sanitize', $SanitizeProvider); @@ -494,14 +526,25 @@ angular.module('ngSanitize', []).provider('$sanitize', $SanitizeProvider); * @kind function * * @description - * Finds links in text input and turns them into html links. Supports http/https/ftp/mailto and + * Finds links in text input and turns them into html links. Supports `http/https/ftp/mailto` and * plain email address links. * * Requires the {@link ngSanitize `ngSanitize`} module to be installed. * * @param {string} text Input text. - * @param {string} target Window (_blank|_self|_parent|_top) or named frame to open links in. - * @returns {string} Html-linkified text. + * @param {string} target Window (`_blank|_self|_parent|_top`) or named frame to open links in. + * @param {object|function(url)} [attributes] Add custom attributes to the link element. + * + * Can be one of: + * + * - `object`: A map of attributes + * - `function`: Takes the url as a parameter and returns a map of attributes + * + * If the map of attributes contains a value for `target`, it overrides the value of + * the target parameter. + * + * + * @returns {string} Html-linkified and {@link $sanitize sanitized} text. * * @usage @@ -509,25 +552,13 @@ angular.module('ngSanitize', []).provider('$sanitize', $SanitizeProvider); * @example -
        Snippet: - - - + + + @@ -541,10 +572,19 @@ angular.module('ngSanitize', []).provider('$sanitize', $SanitizeProvider); + + + + + @@ -554,6 +594,18 @@ angular.module('ngSanitize', []).provider('$sanitize', $SanitizeProvider);
        FilterSourceRenderedFilterSourceRendered
        linky filter
        linky target -
        <div ng-bind-html="snippetWithTarget | linky:'_blank'">
        </div>
        +
        <div ng-bind-html="snippetWithSingleURL | linky:'_blank'">
        </div>
        -
        +
        +
        linky custom attributes +
        <div ng-bind-html="snippetWithSingleURL | linky:'_self':{rel: 'nofollow'}">
        </div>
        +
        +
        + + angular.module('linkyExample', ['ngSanitize']) + .controller('ExampleController', ['$scope', function($scope) { + $scope.snippet = + 'Pretty text with some links:\n'+ + 'http://angularjs.org/,\n'+ + 'mailto:us@somewhere.org,\n'+ + 'another@somewhere.org,\n'+ + 'and one more: ftp://127.0.0.1/.'; + $scope.snippetWithSingleURL = 'http://angularjs.org/'; + }]); + it('should linkify the snippet with urls', function() { expect(element(by.id('linky-filter')).element(by.binding('snippet | linky')).getText()). @@ -581,20 +633,32 @@ angular.module('ngSanitize', []).provider('$sanitize', $SanitizeProvider); it('should work with the target property', function() { expect(element(by.id('linky-target')). - element(by.binding("snippetWithTarget | linky:'_blank'")).getText()). + element(by.binding("snippetWithSingleURL | linky:'_blank'")).getText()). toBe('http://angularjs.org/'); expect(element(by.css('#linky-target a')).getAttribute('target')).toEqual('_blank'); }); + + it('should optionally add custom attributes', function() { + expect(element(by.id('linky-custom-attributes')). + element(by.binding("snippetWithSingleURL | linky:'_self':{rel: 'nofollow'}")).getText()). + toBe('http://angularjs.org/'); + expect(element(by.css('#linky-custom-attributes a')).getAttribute('rel')).toEqual('nofollow'); + }); */ angular.module('ngSanitize').filter('linky', ['$sanitize', function($sanitize) { var LINKY_URL_REGEXP = - /((ftp|https?):\/\/|(mailto:)?[A-Za-z0-9._%+-]+@)\S*[^\s.;,(){}<>]/, - MAILTO_REGEXP = /^mailto:/; + /((ftp|https?):\/\/|(www\.)|(mailto:)?[A-Za-z0-9._%+-]+@)\S*[^\s.;,(){}<>"\u201d\u2019]/i, + MAILTO_REGEXP = /^mailto:/i; + + var linkyMinErr = angular.$$minErr('linky'); + var isString = angular.isString; + + return function(text, target, attributes) { + if (text == null || text === '') return text; + if (!isString(text)) throw linkyMinErr('notstring', 'Expected string but received: {0}', text); - return function(text, target) { - if (!text) return text; var match; var raw = text; var html = []; @@ -603,8 +667,10 @@ angular.module('ngSanitize').filter('linky', ['$sanitize', function($sanitize) { while ((match = raw.match(LINKY_URL_REGEXP))) { // We can not end in these as they are sometimes found at the end of the sentence url = match[0]; - // if we did not match ftp/http/mailto then assume mailto - if (match[2] == match[3]) url = 'mailto:' + url; + // if we did not match ftp/http/www/mailto then assume mailto + if (!match[2] && !match[4]) { + url = (match[3] ? 'http://' : 'mailto:') + url; + } i = match.index; addText(raw.substr(0, i)); addLink(url, match[0].replace(MAILTO_REGEXP, '')); @@ -621,15 +687,26 @@ angular.module('ngSanitize').filter('linky', ['$sanitize', function($sanitize) { } function addLink(url, text) { + var key; html.push(''); + if (angular.isObject(attributes)) { + for (key in attributes) { + html.push(key + '="' + attributes[key] + '" '); + } + } else { + attributes = {}; + } + if (angular.isDefined(target) && !('target' in attributes)) { + html.push('target="', + target, + '" '); + } + html.push('href="', + url.replace(/"/g, '"'), + '">'); addText(text); html.push(''); } diff --git a/app/bower_components/angular-sanitize/angular-sanitize.min.js b/app/bower_components/angular-sanitize/angular-sanitize.min.js index b7f8105..7402a72 100644 --- a/app/bower_components/angular-sanitize/angular-sanitize.min.js +++ b/app/bower_components/angular-sanitize/angular-sanitize.min.js @@ -1,15 +1,15 @@ /* - AngularJS v1.2.21 - (c) 2010-2014 Google, Inc. http://angularjs.org + AngularJS v1.5.6-build.4801+sha.3b5751d + (c) 2010-2016 Google, Inc. http://angularjs.org License: MIT */ -(function(q,g,r){'use strict';function F(a){var d=[];t(d,g.noop).chars(a);return d.join("")}function m(a){var d={};a=a.split(",");var b;for(b=0;b=b;e--)d.end&&d.end(f[e]);f.length=b}}var c,l,f=[],n=a,h;for(f.last=function(){return f[f.length-1]};a;){h="";l=!0;if(f.last()&&y[f.last()])a=a.replace(RegExp("(.*)<\\s*\\/\\s*"+f.last()+"[^>]*>","i"),function(c,a){a=a.replace(I,"$1").replace(J,"$1");d.chars&&d.chars(s(a));return""}),e("",f.last());else{if(0===a.indexOf("\x3c!--"))c=a.indexOf("--",4),0<=c&&a.lastIndexOf("--\x3e",c)===c&&(d.comment&&d.comment(a.substring(4,c)),a=a.substring(c+3),l=!1);else if(z.test(a)){if(c= -a.match(z))a=a.replace(c[0],""),l=!1}else if(K.test(a)){if(c=a.match(A))a=a.substring(c[0].length),c[0].replace(A,e),l=!1}else L.test(a)&&((c=a.match(B))?(c[4]&&(a=a.substring(c[0].length),c[0].replace(B,b)),l=!1):(h+="<",a=a.substring(1)));l&&(c=a.indexOf("<"),h+=0>c?a:a.substring(0,c),a=0>c?"":a.substring(c),d.chars&&d.chars(s(h)))}if(a==n)throw M("badparse",a);n=a}e()}function s(a){if(!a)return"";var d=N.exec(a);a=d[1];var b=d[3];if(d=d[2])p.innerHTML=d.replace(//g,">")}function t(a,d){var b=!1,e=g.bind(a,a.push);return{start:function(a,l,f){a=g.lowercase(a);!b&&y[a]&&(b=a);b||!0!==D[a]||(e("<"),e(a),g.forEach(l,function(b,f){var k=g.lowercase(f),l="img"===a&&"src"===k||"background"=== -k;!0!==Q[k]||!0===E[k]&&!d(b,l)||(e(" "),e(f),e('="'),e(C(b)),e('"'))}),e(f?"/>":">"))},end:function(a){a=g.lowercase(a);b||!0!==D[a]||(e(""));a==b&&(b=!1)},chars:function(a){b||e(C(a))}}}var M=g.$$minErr("$sanitize"),B=/^<((?:[a-zA-Z])[\w:-]*)((?:\s+[\w:-]+(?:\s*=\s*(?:(?:"[^"]*")|(?:'[^']*')|[^>\s]+))?)*)\s*(\/?)\s*(>?)/,A=/^<\/\s*([\w:-]+)[^>]*>/,H=/([\w:-]+)(?:\s*=\s*(?:(?:"((?:[^"])*)")|(?:'((?:[^'])*)')|([^>\s]+)))?/g,L=/^]*?)>/i, -J=/]/,b=/^mailto:/;return function(e,c){function l(a){a&&k.push(F(a))}function f(a,b){k.push("');l(b);k.push("")} -if(!e)return e;for(var n,h=e,k=[],m,p;n=h.match(d);)m=n[0],n[2]==n[3]&&(m="mailto:"+m),p=n.index,l(h.substr(0,p)),f(m,n[0].replace(b,"")),h=h.substring(p+n[0].length);l(h);return a(k.join(""))}}])})(window,window.angular); +(function(n,e){'use strict';function B(a){var c=[];w(c,e.noop).chars(a);return c.join("")}function h(a,c){var b={},d=a.split(","),l;for(l=0;l/g,">")}function w(a,c){var b=!1,d=e.bind(a,a.push);return{start:function(a,f){a=e.lowercase(a);!b&&G[a]&&(b=a);b||!0!==u[a]||(d("<"),d(a),e.forEach(f,function(b,f){var g=e.lowercase(f),h="img"===a&&"src"===g||"background"===g;!0!==H[g]||!0===z[g]&&!c(b,h)||(d(" "),d(f),d('="'),d(y(b)),d('"'))}),d(">"))},end:function(a){a=e.lowercase(a);b||!0!==u[a]||!0===A[a]||(d(""));a== +b&&(b=!1)},chars:function(a){b||d(y(a))}}}function t(a){if(a.nodeType===n.Node.ELEMENT_NODE)for(var c=a.attributes,b=0,d=c.length;b"\u201d\u2019]/i,b=/^mailto:/i,d=e.$$minErr("linky"),g=e.isString;return function(f,h,m){function k(a){a&&p.push(B(a))}function q(a,b){var c;p.push("');k(b);p.push("")}if(null==f||""===f)return f;if(!g(f))throw d("notstring",f);for(var r=f,p=[],s,n;f=r.match(c);)s=f[0],f[2]||f[4]||(s=(f[3]?"http://":"mailto:")+s),n=f.index,k(r.substr(0,n)),q(s,f[0].replace(b,"")),r=r.substring(n+f[0].length);k(r);return a(p.join(""))}}])})(window,window.angular); //# sourceMappingURL=angular-sanitize.min.js.map diff --git a/app/bower_components/angular-sanitize/angular-sanitize.min.js.map b/app/bower_components/angular-sanitize/angular-sanitize.min.js.map index 1b4a750..4bfc736 100644 --- a/app/bower_components/angular-sanitize/angular-sanitize.min.js.map +++ b/app/bower_components/angular-sanitize/angular-sanitize.min.js.map @@ -2,7 +2,7 @@ "version":3, "file":"angular-sanitize.min.js", "lineCount":14, -"mappings":"A;;;;;aAKC,SAAQ,CAACA,CAAD,CAASC,CAAT,CAAkBC,CAAlB,CAA6B,CAkJtCC,QAASA,EAAY,CAACC,CAAD,CAAQ,CAC3B,IAAIC,EAAM,EACGC,EAAAC,CAAmBF,CAAnBE,CAAwBN,CAAAO,KAAxBD,CACbH,MAAA,CAAaA,CAAb,CACA,OAAOC,EAAAI,KAAA,CAAS,EAAT,CAJoB,CAoE7BC,QAASA,EAAO,CAACC,CAAD,CAAM,CAAA,IAChBC,EAAM,EAAIC,EAAAA,CAAQF,CAAAG,MAAA,CAAU,GAAV,CAAtB,KAAsCC,CACtC,KAAKA,CAAL,CAAS,CAAT,CAAYA,CAAZ,CAAgBF,CAAAG,OAAhB,CAA8BD,CAAA,EAA9B,CAAmCH,CAAA,CAAIC,CAAA,CAAME,CAAN,CAAJ,CAAA,CAAgB,CAAA,CACnD,OAAOH,EAHa,CAmBtBK,QAASA,EAAU,CAAEC,CAAF,CAAQC,CAAR,CAAkB,CAyFnCC,QAASA,EAAa,CAAEC,CAAF,CAAOC,CAAP,CAAgBC,CAAhB,CAAsBC,CAAtB,CAA8B,CAClDF,CAAA,CAAUrB,CAAAwB,UAAA,CAAkBH,CAAlB,CACV,IAAKI,CAAA,CAAeJ,CAAf,CAAL,CACE,IAAA,CAAQK,CAAAC,KAAA,EAAR,EAAwBC,CAAA,CAAgBF,CAAAC,KAAA,EAAhB,CAAxB,CAAA,CACEE,CAAA,CAAa,EAAb,CAAiBH,CAAAC,KAAA,EAAjB,CAICG,EAAA,CAAwBT,CAAxB,CAAL,EAA0CK,CAAAC,KAAA,EAA1C,EAA0DN,CAA1D,EACEQ,CAAA,CAAa,EAAb,CAAiBR,CAAjB,CAKF,EAFAE,CAEA,CAFQQ,CAAA,CAAcV,CAAd,CAER,EAFmC,CAAC,CAACE,CAErC,GACEG,CAAAM,KAAA,CAAYX,CAAZ,CAEF,KAAIY,EAAQ,EAEZX,EAAAY,QAAA,CAAaC,CAAb,CACE,QAAQ,CAACC,CAAD,CAAQC,CAAR,CAAcC,CAAd,CAAiCC,CAAjC,CAAoDC,CAApD,CAAmE,CAMzEP,CAAA,CAAMI,CAAN,CAAA,CAAcI,CAAA,CALFH,CAKE,EAJTC,CAIS,EAHTC,CAGS,EAFT,EAES,CAN2D,CAD7E,CASItB,EAAAwB,MAAJ,EAAmBxB,CAAAwB,MAAA,CAAerB,CAAf,CAAwBY,CAAxB,CAA+BV,CAA/B,CA5B+B,CA+BpDM,QAASA,EAAW,CAAET,CAAF,CAAOC,CAAP,CAAiB,CAAA,IAC/BsB,EAAM,CADyB,CACtB7B,CAEb,IADAO,CACA,CADUrB,CAAAwB,UAAA,CAAkBH,CAAlB,CACV,CAEE,IAAMsB,CAAN,CAAYjB,CAAAX,OAAZ,CAA2B,CAA3B,CAAqC,CAArC,EAA8B4B,CAA9B,EACOjB,CAAA,CAAOiB,CAAP,CADP,EACuBtB,CADvB,CAAwCsB,CAAA,EAAxC;AAIF,GAAY,CAAZ,EAAKA,CAAL,CAAgB,CAEd,IAAM7B,CAAN,CAAUY,CAAAX,OAAV,CAAyB,CAAzB,CAA4BD,CAA5B,EAAiC6B,CAAjC,CAAsC7B,CAAA,EAAtC,CACMI,CAAA0B,IAAJ,EAAiB1B,CAAA0B,IAAA,CAAalB,CAAA,CAAOZ,CAAP,CAAb,CAGnBY,EAAAX,OAAA,CAAe4B,CAND,CATmB,CAxHF,IAC/BE,CAD+B,CACxB1C,CADwB,CACVuB,EAAQ,EADE,CACEC,EAAOV,CADT,CACe6B,CAGlD,KAFApB,CAAAC,KAEA,CAFaoB,QAAQ,EAAG,CAAE,MAAOrB,EAAA,CAAOA,CAAAX,OAAP,CAAsB,CAAtB,CAAT,CAExB,CAAQE,CAAR,CAAA,CAAe,CACb6B,CAAA,CAAO,EACP3C,EAAA,CAAQ,CAAA,CAGR,IAAMuB,CAAAC,KAAA,EAAN,EAAuBqB,CAAA,CAAiBtB,CAAAC,KAAA,EAAjB,CAAvB,CA0DEV,CASA,CATOA,CAAAiB,QAAA,CAAiBe,MAAJ,CAAW,kBAAX,CAAgCvB,CAAAC,KAAA,EAAhC,CAA+C,QAA/C,CAAyD,GAAzD,CAAb,CACL,QAAQ,CAACuB,CAAD,CAAMJ,CAAN,CAAW,CACjBA,CAAA,CAAOA,CAAAZ,QAAA,CAAaiB,CAAb,CAA6B,IAA7B,CAAAjB,QAAA,CAA2CkB,CAA3C,CAAyD,IAAzD,CAEHlC,EAAAf,MAAJ,EAAmBe,CAAAf,MAAA,CAAesC,CAAA,CAAeK,CAAf,CAAf,CAEnB,OAAO,EALU,CADd,CASP,CAAAjB,CAAA,CAAa,EAAb,CAAiBH,CAAAC,KAAA,EAAjB,CAnEF,KAAyD,CAGvD,GAA8B,CAA9B,GAAKV,CAAAoC,QAAA,CAAa,SAAb,CAAL,CAEER,CAEA,CAFQ5B,CAAAoC,QAAA,CAAa,IAAb,CAAmB,CAAnB,CAER,CAAc,CAAd,EAAKR,CAAL,EAAmB5B,CAAAqC,YAAA,CAAiB,QAAjB,CAAwBT,CAAxB,CAAnB,GAAsDA,CAAtD,GACM3B,CAAAqC,QAEJ,EAFqBrC,CAAAqC,QAAA,CAAiBtC,CAAAuC,UAAA,CAAgB,CAAhB,CAAmBX,CAAnB,CAAjB,CAErB,CADA5B,CACA,CADOA,CAAAuC,UAAA,CAAgBX,CAAhB,CAAwB,CAAxB,CACP,CAAA1C,CAAA,CAAQ,CAAA,CAHV,CAJF,KAUO,IAAKsD,CAAAC,KAAA,CAAoBzC,CAApB,CAAL,CAGL,IAFAmB,CAEA;AAFQnB,CAAAmB,MAAA,CAAYqB,CAAZ,CAER,CACExC,CACA,CADOA,CAAAiB,QAAA,CAAcE,CAAA,CAAM,CAAN,CAAd,CAAwB,EAAxB,CACP,CAAAjC,CAAA,CAAQ,CAAA,CAFV,CAHK,IAQA,IAAKwD,CAAAD,KAAA,CAA4BzC,CAA5B,CAAL,CAGL,IAFAmB,CAEA,CAFQnB,CAAAmB,MAAA,CAAYwB,CAAZ,CAER,CACE3C,CAEA,CAFOA,CAAAuC,UAAA,CAAgBpB,CAAA,CAAM,CAAN,CAAArB,OAAhB,CAEP,CADAqB,CAAA,CAAM,CAAN,CAAAF,QAAA,CAAkB0B,CAAlB,CAAkC/B,CAAlC,CACA,CAAA1B,CAAA,CAAQ,CAAA,CAHV,CAHK,IAUK0D,EAAAH,KAAA,CAAsBzC,CAAtB,CAAL,GAGL,CAFAmB,CAEA,CAFQnB,CAAAmB,MAAA,CAAY0B,CAAZ,CAER,GAEO1B,CAAA,CAAM,CAAN,CAIL,GAHEnB,CACA,CADOA,CAAAuC,UAAA,CAAgBpB,CAAA,CAAM,CAAN,CAAArB,OAAhB,CACP,CAAAqB,CAAA,CAAM,CAAN,CAAAF,QAAA,CAAkB4B,CAAlB,CAAoC3C,CAApC,CAEF,EAAAhB,CAAA,CAAQ,CAAA,CANV,GASE2C,CACA,EADQ,GACR,CAAA7B,CAAA,CAAOA,CAAAuC,UAAA,CAAe,CAAf,CAVT,CAHK,CAiBFrD,EAAL,GACE0C,CAKA,CALQ5B,CAAAoC,QAAA,CAAa,GAAb,CAKR,CAHAP,CAGA,EAHgB,CAAR,CAAAD,CAAA,CAAY5B,CAAZ,CAAmBA,CAAAuC,UAAA,CAAgB,CAAhB,CAAmBX,CAAnB,CAG3B,CAFA5B,CAEA,CAFe,CAAR,CAAA4B,CAAA,CAAY,EAAZ,CAAiB5B,CAAAuC,UAAA,CAAgBX,CAAhB,CAExB,CAAI3B,CAAAf,MAAJ,EAAmBe,CAAAf,MAAA,CAAesC,CAAA,CAAeK,CAAf,CAAf,CANrB,CAhDuD,CAsEzD,GAAK7B,CAAL,EAAaU,CAAb,CACE,KAAMoC,EAAA,CAAgB,UAAhB,CAC4C9C,CAD5C,CAAN,CAGFU,CAAA,CAAOV,CA/EM,CAmFfY,CAAA,EAvFmC,CAmJrCY,QAASA,EAAc,CAACuB,CAAD,CAAQ,CAC7B,GAAI,CAACA,CAAL,CAAc,MAAO,EAIrB,KAAIC,EAAQC,CAAAC,KAAA,CAAaH,CAAb,CACRI,EAAAA,CAAcH,CAAA,CAAM,CAAN,CAClB,KAAII,EAAaJ,CAAA,CAAM,CAAN,CAEjB,IADIK,CACJ,CADcL,CAAA,CAAM,CAAN,CACd,CACEM,CAAAC,UAKA,CALoBF,CAAApC,QAAA,CAAgB,IAAhB,CAAqB,MAArB,CAKpB,CAAAoC,CAAA,CAAU,aAAA,EAAiBC,EAAjB;AACRA,CAAAE,YADQ,CACgBF,CAAAG,UAE5B,OAAON,EAAP,CAAqBE,CAArB,CAA+BD,CAlBF,CA4B/BM,QAASA,EAAc,CAACX,CAAD,CAAQ,CAC7B,MAAOA,EAAA9B,QAAA,CACG,IADH,CACS,OADT,CAAAA,QAAA,CAEG0C,CAFH,CAE0B,QAAS,CAACZ,CAAD,CAAQ,CAC9C,IAAIa,EAAKb,CAAAc,WAAA,CAAiB,CAAjB,CACLC,EAAAA,CAAMf,CAAAc,WAAA,CAAiB,CAAjB,CACV,OAAO,IAAP,EAAgC,IAAhC,EAAiBD,CAAjB,CAAsB,KAAtB,GAA0CE,CAA1C,CAAgD,KAAhD,EAA0D,KAA1D,EAAqE,GAHvB,CAF3C,CAAA7C,QAAA,CAOG8C,CAPH,CAO4B,QAAQ,CAAChB,CAAD,CAAO,CAC9C,MAAO,IAAP,CAAcA,CAAAc,WAAA,CAAiB,CAAjB,CAAd,CAAoC,GADU,CAP3C,CAAA5C,QAAA,CAUG,IAVH,CAUS,MAVT,CAAAA,QAAA,CAWG,IAXH,CAWS,MAXT,CADsB,CAyB/B7B,QAASA,EAAkB,CAACD,CAAD,CAAM6E,CAAN,CAAmB,CAC5C,IAAIC,EAAS,CAAA,CAAb,CACIC,EAAMnF,CAAAoF,KAAA,CAAahF,CAAb,CAAkBA,CAAA4B,KAAlB,CACV,OAAO,OACEU,QAAQ,CAACtB,CAAD,CAAMa,CAAN,CAAaV,CAAb,CAAmB,CAChCH,CAAA,CAAMpB,CAAAwB,UAAA,CAAkBJ,CAAlB,CACD8D,EAAAA,CAAL,EAAelC,CAAA,CAAgB5B,CAAhB,CAAf,GACE8D,CADF,CACW9D,CADX,CAGK8D,EAAL,EAAsC,CAAA,CAAtC,GAAeG,CAAA,CAAcjE,CAAd,CAAf,GACE+D,CAAA,CAAI,GAAJ,CAcA,CAbAA,CAAA,CAAI/D,CAAJ,CAaA,CAZApB,CAAAsF,QAAA,CAAgBrD,CAAhB,CAAuB,QAAQ,CAAC+B,CAAD,CAAQuB,CAAR,CAAY,CACzC,IAAIC,EAAKxF,CAAAwB,UAAA,CAAkB+D,CAAlB,CAAT,CACIE,EAAmB,KAAnBA,GAAWrE,CAAXqE,EAAqC,KAArCA,GAA4BD,CAA5BC,EAAyD,YAAzDA;AAAgDD,CAC3B,EAAA,CAAzB,GAAIE,CAAA,CAAWF,CAAX,CAAJ,EACsB,CAAA,CADtB,GACGG,CAAA,CAASH,CAAT,CADH,EAC8B,CAAAP,CAAA,CAAajB,CAAb,CAAoByB,CAApB,CAD9B,GAEEN,CAAA,CAAI,GAAJ,CAIA,CAHAA,CAAA,CAAII,CAAJ,CAGA,CAFAJ,CAAA,CAAI,IAAJ,CAEA,CADAA,CAAA,CAAIR,CAAA,CAAeX,CAAf,CAAJ,CACA,CAAAmB,CAAA,CAAI,GAAJ,CANF,CAHyC,CAA3C,CAYA,CAAAA,CAAA,CAAI5D,CAAA,CAAQ,IAAR,CAAe,GAAnB,CAfF,CALgC,CAD7B,KAwBAqB,QAAQ,CAACxB,CAAD,CAAK,CACdA,CAAA,CAAMpB,CAAAwB,UAAA,CAAkBJ,CAAlB,CACD8D,EAAL,EAAsC,CAAA,CAAtC,GAAeG,CAAA,CAAcjE,CAAd,CAAf,GACE+D,CAAA,CAAI,IAAJ,CAEA,CADAA,CAAA,CAAI/D,CAAJ,CACA,CAAA+D,CAAA,CAAI,GAAJ,CAHF,CAKI/D,EAAJ,EAAW8D,CAAX,GACEA,CADF,CACW,CAAA,CADX,CAPc,CAxBb,OAmCE/E,QAAQ,CAACA,CAAD,CAAO,CACb+E,CAAL,EACEC,CAAA,CAAIR,CAAA,CAAexE,CAAf,CAAJ,CAFgB,CAnCjB,CAHqC,CA/a9C,IAAI4D,EAAkB/D,CAAA4F,SAAA,CAAiB,WAAjB,CAAtB,CAyJI9B,EACG,wGA1JP,CA2JEF,EAAiB,wBA3JnB,CA4JEzB,EAAc,yEA5JhB,CA6JE0B,EAAmB,IA7JrB,CA8JEF,EAAyB,MA9J3B,CA+JER,EAAiB,qBA/JnB,CAgKEM,EAAiB,qBAhKnB;AAiKEL,EAAe,yBAjKjB,CAkKEwB,EAAwB,iCAlK1B,CAoKEI,EAA0B,gBApK5B,CA6KIjD,EAAetB,CAAA,CAAQ,wBAAR,CAIfoF,EAAAA,CAA8BpF,CAAA,CAAQ,gDAAR,CAC9BqF,EAAAA,CAA+BrF,CAAA,CAAQ,OAAR,CADnC,KAEIqB,EAAyB9B,CAAA+F,OAAA,CAAe,EAAf,CACeD,CADf,CAEeD,CAFf,CAF7B,CAOIpE,EAAgBzB,CAAA+F,OAAA,CAAe,EAAf,CAAmBF,CAAnB,CAAgDpF,CAAA,CAAQ,4KAAR,CAAhD,CAPpB,CAYImB,EAAiB5B,CAAA+F,OAAA,CAAe,EAAf,CAAmBD,CAAnB,CAAiDrF,CAAA,CAAQ,2JAAR,CAAjD,CAZrB;AAkBIuC,EAAkBvC,CAAA,CAAQ,cAAR,CAlBtB,CAoBI4E,EAAgBrF,CAAA+F,OAAA,CAAe,EAAf,CACehE,CADf,CAEeN,CAFf,CAGeG,CAHf,CAIeE,CAJf,CApBpB,CA2BI6D,EAAWlF,CAAA,CAAQ,0CAAR,CA3Bf,CA4BIiF,EAAa1F,CAAA+F,OAAA,CAAe,EAAf,CAAmBJ,CAAnB,CAA6BlF,CAAA,CAC1C,ySAD0C,CAA7B,CA5BjB,CAkMI8D,EAAUyB,QAAAC,cAAA,CAAuB,KAAvB,CAlMd,CAmMI/B,EAAU,wBA2GdlE,EAAAkG,OAAA,CAAe,YAAf,CAA6B,EAA7B,CAAAC,SAAA,CAA0C,WAA1C;AA3VAC,QAA0B,EAAG,CAC3B,IAAAC,KAAA,CAAY,CAAC,eAAD,CAAkB,QAAQ,CAACC,CAAD,CAAgB,CACpD,MAAO,SAAQ,CAACrF,CAAD,CAAO,CACpB,IAAIb,EAAM,EACVY,EAAA,CAAWC,CAAX,CAAiBZ,CAAA,CAAmBD,CAAnB,CAAwB,QAAQ,CAACmG,CAAD,CAAMd,CAAN,CAAe,CAC9D,MAAO,CAAC,SAAA/B,KAAA,CAAe4C,CAAA,CAAcC,CAAd,CAAmBd,CAAnB,CAAf,CADsD,CAA/C,CAAjB,CAGA,OAAOrF,EAAAI,KAAA,CAAS,EAAT,CALa,CAD8B,CAA1C,CADe,CA2V7B,CAwGAR,EAAAkG,OAAA,CAAe,YAAf,CAAAM,OAAA,CAAoC,OAApC,CAA6C,CAAC,WAAD,CAAc,QAAQ,CAACC,CAAD,CAAY,CAAA,IACzEC,EACE,mEAFuE,CAGzEC,EAAgB,UAEpB,OAAO,SAAQ,CAAC7D,CAAD,CAAO8D,CAAP,CAAe,CAoB5BC,QAASA,EAAO,CAAC/D,CAAD,CAAO,CAChBA,CAAL,EAGA7B,CAAAe,KAAA,CAAU9B,CAAA,CAAa4C,CAAb,CAAV,CAJqB,CAOvBgE,QAASA,EAAO,CAACC,CAAD,CAAMjE,CAAN,CAAY,CAC1B7B,CAAAe,KAAA,CAAU,KAAV,CACIhC,EAAAgH,UAAA,CAAkBJ,CAAlB,CAAJ,GACE3F,CAAAe,KAAA,CAAU,UAAV,CAEA,CADAf,CAAAe,KAAA,CAAU4E,CAAV,CACA,CAAA3F,CAAAe,KAAA,CAAU,IAAV,CAHF,CAKAf,EAAAe,KAAA,CAAU,QAAV,CACAf,EAAAe,KAAA,CAAU+E,CAAV,CACA9F,EAAAe,KAAA,CAAU,IAAV,CACA6E,EAAA,CAAQ/D,CAAR,CACA7B,EAAAe,KAAA,CAAU,MAAV,CAX0B,CA3BA;AAC5B,GAAI,CAACc,CAAL,CAAW,MAAOA,EAMlB,KALA,IAAIV,CAAJ,CACI6E,EAAMnE,CADV,CAEI7B,EAAO,EAFX,CAGI8F,CAHJ,CAIIjG,CACJ,CAAQsB,CAAR,CAAgB6E,CAAA7E,MAAA,CAAUsE,CAAV,CAAhB,CAAA,CAEEK,CAMA,CANM3E,CAAA,CAAM,CAAN,CAMN,CAJIA,CAAA,CAAM,CAAN,CAIJ,EAJgBA,CAAA,CAAM,CAAN,CAIhB,GAJ0B2E,CAI1B,CAJgC,SAIhC,CAJ4CA,CAI5C,EAHAjG,CAGA,CAHIsB,CAAAS,MAGJ,CAFAgE,CAAA,CAAQI,CAAAC,OAAA,CAAW,CAAX,CAAcpG,CAAd,CAAR,CAEA,CADAgG,CAAA,CAAQC,CAAR,CAAa3E,CAAA,CAAM,CAAN,CAAAF,QAAA,CAAiByE,CAAjB,CAAgC,EAAhC,CAAb,CACA,CAAAM,CAAA,CAAMA,CAAAzD,UAAA,CAAc1C,CAAd,CAAkBsB,CAAA,CAAM,CAAN,CAAArB,OAAlB,CAER8F,EAAA,CAAQI,CAAR,CACA,OAAOR,EAAA,CAAUxF,CAAAT,KAAA,CAAU,EAAV,CAAV,CAlBqB,CAL+C,CAAlC,CAA7C,CAzkBsC,CAArC,CAAA,CA0nBET,MA1nBF,CA0nBUA,MAAAC,QA1nBV;", +"mappings":"A;;;;;aAKC,SAAQ,CAACA,CAAD,CAASC,CAAT,CAAkB,CAsM3BC,QAASA,EAAY,CAACC,CAAD,CAAQ,CAC3B,IAAIC,EAAM,EACGC,EAAAC,CAAmBF,CAAnBE,CAAwBL,CAAAM,KAAxBD,CACbH,MAAA,CAAaA,CAAb,CACA,OAAOC,EAAAI,KAAA,CAAS,EAAT,CAJoB,CAyF7BC,QAASA,EAAK,CAACC,CAAD,CAAMC,CAAN,CAAqB,CAAA,IAC7BC,EAAM,EADuB,CACnBC,EAAQH,CAAAI,MAAA,CAAU,GAAV,CADW,CACKC,CACtC,KAAKA,CAAL,CAAS,CAAT,CAAYA,CAAZ,CAAgBF,CAAAG,OAAhB,CAA8BD,CAAA,EAA9B,CACEH,CAAA,CAAID,CAAA,CAAgBV,CAAAgB,UAAA,CAAkBJ,CAAA,CAAME,CAAN,CAAlB,CAAhB,CAA8CF,CAAA,CAAME,CAAN,CAAlD,CAAA,CAA8D,CAAA,CAEhE,OAAOH,EAL0B,CA0CnCM,QAASA,EAAU,CAACC,CAAD,CAAOC,CAAP,CAAgB,CACpB,IAAb,GAAID,CAAJ,EAA8BE,IAAAA,EAA9B,GAAqBF,CAArB,CACEA,CADF,CACS,EADT,CAE2B,QAF3B,GAEW,MAAOA,EAFlB,GAGEA,CAHF,CAGS,EAHT,CAGcA,CAHd,CAKAG,EAAAC,UAAA,CAA6BJ,CAG7B,KAAIK,EAAe,CACnB,GAAG,CACD,GAAqB,CAArB,GAAIA,CAAJ,CACE,KAAMC,EAAA,CAAgB,QAAhB,CAAN,CAEFD,CAAA,EAGIxB,EAAA0B,SAAAC,aAAJ,EACEC,CAAA,CAAmBN,CAAnB,CAEFH,EAAA,CAAOG,CAAAC,UACPD,EAAAC,UAAA,CAA6BJ,CAX5B,CAAH,MAYSA,CAZT,GAYkBG,CAAAC,UAZlB,CAeA,KADIM,CACJ,CADWP,CAAAQ,WACX,CAAOD,CAAP,CAAA,CAAa,CACX,OAAQA,CAAAE,SAAR,EACE,KAAK,CAAL,CACEX,CAAAY,MAAA,CAAcH,CAAAI,SAAAC,YAAA,EAAd,CAA2CC,CAAA,CAAUN,CAAAO,WAAV,CAA3C,CACA;KACF,MAAK,CAAL,CACEhB,CAAAjB,MAAA,CAAc0B,CAAAQ,YAAd,CALJ,CASA,IAAIC,CACJ,IAAM,EAAAA,CAAA,CAAWT,CAAAC,WAAX,CAAN,GACwB,CAIjBQ,GAJDT,CAAAE,SAICO,EAHHlB,CAAAmB,IAAA,CAAYV,CAAAI,SAAAC,YAAA,EAAZ,CAGGI,CADLA,CACKA,CADMT,CAAAW,YACNF,CAAAA,CAAAA,CALP,EAMI,IAAA,CAAmB,IAAnB,EAAOA,CAAP,CAAA,CAAyB,CACvBT,CAAA,CAAOA,CAAAY,WACP,IAAIZ,CAAJ,GAAaP,CAAb,CAA+B,KAC/BgB,EAAA,CAAWT,CAAAW,YACW,EAAtB,GAAIX,CAAAE,SAAJ,EACEX,CAAAmB,IAAA,CAAYV,CAAAI,SAAAC,YAAA,EAAZ,CALqB,CAU7BL,CAAA,CAAOS,CA3BI,CA8Bb,IAAA,CAAOT,CAAP,CAAcP,CAAAQ,WAAd,CAAA,CACER,CAAAoB,YAAA,CAA6Bb,CAA7B,CAxD+B,CA4DnCM,QAASA,EAAS,CAACQ,CAAD,CAAQ,CAExB,IADA,IAAIC,EAAM,EAAV,CACS7B,EAAI,CADb,CACgB8B,EAAKF,CAAA3B,OAArB,CAAmCD,CAAnC,CAAuC8B,CAAvC,CAA2C9B,CAAA,EAA3C,CAAgD,CAC9C,IAAI+B,EAAOH,CAAA,CAAM5B,CAAN,CACX6B,EAAA,CAAIE,CAAAC,KAAJ,CAAA,CAAiBD,CAAAE,MAF6B,CAIhD,MAAOJ,EANiB,CAiB1BK,QAASA,EAAc,CAACD,CAAD,CAAQ,CAC7B,MAAOA,EAAAE,QAAA,CACG,IADH,CACS,OADT,CAAAA,QAAA,CAEGC,CAFH,CAE0B,QAAQ,CAACH,CAAD,CAAQ,CAC7C,IAAII,EAAKJ,CAAAK,WAAA,CAAiB,CAAjB,CACLC,EAAAA,CAAMN,CAAAK,WAAA,CAAiB,CAAjB,CACV,OAAO,IAAP,EAAgC,IAAhC,EAAiBD,CAAjB,CAAsB,KAAtB;CAA0CE,CAA1C,CAAgD,KAAhD,EAA0D,KAA1D,EAAqE,GAHxB,CAF1C,CAAAJ,QAAA,CAOGK,CAPH,CAO4B,QAAQ,CAACP,CAAD,CAAQ,CAC/C,MAAO,IAAP,CAAcA,CAAAK,WAAA,CAAiB,CAAjB,CAAd,CAAoC,GADW,CAP5C,CAAAH,QAAA,CAUG,IAVH,CAUS,MAVT,CAAAA,QAAA,CAWG,IAXH,CAWS,MAXT,CADsB,CAyB/B7C,QAASA,EAAkB,CAACD,CAAD,CAAMoD,CAAN,CAAoB,CAC7C,IAAIC,EAAuB,CAAA,CAA3B,CACIC,EAAMzD,CAAA0D,KAAA,CAAavD,CAAb,CAAkBA,CAAAwD,KAAlB,CACV,OAAO,CACL5B,MAAOA,QAAQ,CAAC6B,CAAD,CAAMlB,CAAN,CAAa,CAC1BkB,CAAA,CAAM5D,CAAAgB,UAAA,CAAkB4C,CAAlB,CACDJ,EAAAA,CAAL,EAA6BK,CAAA,CAAgBD,CAAhB,CAA7B,GACEJ,CADF,CACyBI,CADzB,CAGKJ,EAAL,EAAoD,CAAA,CAApD,GAA6BM,CAAA,CAAcF,CAAd,CAA7B,GACEH,CAAA,CAAI,GAAJ,CAcA,CAbAA,CAAA,CAAIG,CAAJ,CAaA,CAZA5D,CAAA+D,QAAA,CAAgBrB,CAAhB,CAAuB,QAAQ,CAACK,CAAD,CAAQiB,CAAR,CAAa,CAC1C,IAAIC,EAAKjE,CAAAgB,UAAA,CAAkBgD,CAAlB,CAAT,CACIE,EAAmB,KAAnBA,GAAWN,CAAXM,EAAqC,KAArCA,GAA4BD,CAA5BC,EAAyD,YAAzDA,GAAgDD,CAC3B,EAAA,CAAzB,GAAIE,CAAA,CAAWF,CAAX,CAAJ,EACsB,CAAA,CADtB,GACGG,CAAA,CAASH,CAAT,CADH,EAC8B,CAAAV,CAAA,CAAaR,CAAb,CAAoBmB,CAApB,CAD9B,GAEET,CAAA,CAAI,GAAJ,CAIA,CAHAA,CAAA,CAAIO,CAAJ,CAGA,CAFAP,CAAA,CAAI,IAAJ,CAEA,CADAA,CAAA,CAAIT,CAAA,CAAeD,CAAf,CAAJ,CACA,CAAAU,CAAA,CAAI,GAAJ,CANF,CAH0C,CAA5C,CAYA,CAAAA,CAAA,CAAI,GAAJ,CAfF,CAL0B,CADvB,CAwBLnB,IAAKA,QAAQ,CAACsB,CAAD,CAAM,CACjBA,CAAA,CAAM5D,CAAAgB,UAAA,CAAkB4C,CAAlB,CACDJ,EAAL,EAAoD,CAAA,CAApD,GAA6BM,CAAA,CAAcF,CAAd,CAA7B,EAAkF,CAAA,CAAlF,GAA4DS,CAAA,CAAaT,CAAb,CAA5D,GACEH,CAAA,CAAI,IAAJ,CAEA,CADAA,CAAA,CAAIG,CAAJ,CACA,CAAAH,CAAA,CAAI,GAAJ,CAHF,CAKIG,EAAJ;AAAWJ,CAAX,GACEA,CADF,CACyB,CAAA,CADzB,CAPiB,CAxBd,CAmCLtD,MAAOA,QAAQ,CAACA,CAAD,CAAQ,CAChBsD,CAAL,EACEC,CAAA,CAAIT,CAAA,CAAe9C,CAAf,CAAJ,CAFmB,CAnClB,CAHsC,CAsD/CyB,QAASA,EAAkB,CAACC,CAAD,CAAO,CAChC,GAAIA,CAAAE,SAAJ,GAAsB/B,CAAAuE,KAAAC,aAAtB,CAEE,IADA,IAAI7B,EAAQd,CAAAO,WAAZ,CACSrB,EAAI,CADb,CACgB0D,EAAI9B,CAAA3B,OAApB,CAAkCD,CAAlC,CAAsC0D,CAAtC,CAAyC1D,CAAA,EAAzC,CAA8C,CAC5C,IAAI2D,EAAW/B,CAAA,CAAM5B,CAAN,CAAf,CACI4D,EAAWD,CAAA3B,KAAAb,YAAA,EACf,IAAiB,WAAjB,GAAIyC,CAAJ,EAA6D,CAA7D,GAAgCA,CAAAC,QAAA,CAAiB,MAAjB,CAAhC,CACE/C,CAAAgD,oBAAA,CAAyBH,CAAzB,CAEA,CADA3D,CAAA,EACA,CAAA0D,CAAA,EAN0C,CAYhD,CADInC,CACJ,CADeT,CAAAC,WACf,GACEF,CAAA,CAAmBU,CAAnB,CAIF,EADAA,CACA,CADWT,CAAAW,YACX,GACEZ,CAAA,CAAmBU,CAAnB,CArB8B,CAxdlC,IAAIb,EAAkBxB,CAAA6E,SAAA,CAAiB,WAAjB,CAAtB,CAkMI3B,EAAwB,iCAlM5B,CAoMEI,EAA0B,eApM5B,CA6MIe,EAAe7D,CAAA,CAAM,wBAAN,CA7MnB,CAiNIsE,EAA8BtE,CAAA,CAAM,gDAAN,CAjNlC,CAkNIuE,EAA+BvE,CAAA,CAAM,OAAN,CAlNnC,CAmNIwE,EAAyBhF,CAAAiF,OAAA,CAAe,EAAf,CACeF,CADf,CAEeD,CAFf,CAnN7B,CAwNII,EAAgBlF,CAAAiF,OAAA,CAAe,EAAf;AAAmBH,CAAnB,CAAgDtE,CAAA,CAAM,qKAAN,CAAhD,CAxNpB,CA6NI2E,EAAiBnF,CAAAiF,OAAA,CAAe,EAAf,CAAmBF,CAAnB,CAAiDvE,CAAA,CAAM,2JAAN,CAAjD,CA7NrB,CAqOI4E,EAAc5E,CAAA,CAAM,wNAAN,CArOlB;AA0OIqD,EAAkBrD,CAAA,CAAM,cAAN,CA1OtB,CA4OIsD,EAAgB9D,CAAAiF,OAAA,CAAe,EAAf,CACeZ,CADf,CAEea,CAFf,CAGeC,CAHf,CAIeH,CAJf,CA5OpB,CAmPIZ,EAAW5D,CAAA,CAAM,8CAAN,CAnPf,CAqPI6E,EAAY7E,CAAA,CAAM,kTAAN,CArPhB,CA6PI8E,EAAW9E,CAAA,CAAM,guCAAN;AAcoE,CAAA,CAdpE,CA7Pf,CA6QI2D,EAAanE,CAAAiF,OAAA,CAAe,EAAf,CACeb,CADf,CAEekB,CAFf,CAGeD,CAHf,CA7QjB,CA0RIhE,CACH,UAAQ,CAACtB,CAAD,CAAS,CAEhB,GAAIA,CAAA0B,SAAJ,EAAuB1B,CAAA0B,SAAA8D,eAAvB,CACEC,CAAA,CAAMzF,CAAA0B,SAAA8D,eAAAE,mBAAA,CAAkD,OAAlD,CADR,KAGE,MAAMjE,EAAA,CAAgB,SAAhB,CAAN,CAGF,IAAIkE,EAAeC,CADFH,CAAAI,gBACED,EADqBH,CAAAK,mBAAA,EACrBF,sBAAA,CAAgC,MAAhC,CAGS,EAA5B,GAAID,CAAA3E,OAAJ,CACEM,CADF,CACqBqE,CAAA,CAAa,CAAb,CADrB,EAGMxE,CAGJ,CAHWsE,CAAAM,cAAA,CAAkB,MAAlB,CAGX,CAFAzE,CAEA,CAFmBmE,CAAAM,cAAA,CAAkB,MAAlB,CAEnB,CADA5E,CAAA6E,YAAA,CAAiB1E,CAAjB,CACA,CAAAmE,CAAAO,YAAA,CAAgB7E,CAAhB,CANF,CAXgB,CAAjB,CAAD,CAmBGnB,CAnBH,CAyNAC,EAAAgG,OAAA,CAAe,YAAf,CAA6B,EAA7B,CAAAC,SAAA,CAA0C,WAA1C,CApXAC,QAA0B,EAAG,CAC3B,IAAIC,EAAa,CAAA,CAEjB,KAAAC,KAAA,CAAY,CAAC,eAAD,CAAkB,QAAQ,CAACC,CAAD,CAAgB,CAChDF,CAAJ,EACEnG,CAAAiF,OAAA,CAAenB,CAAf,CAA8BsB,CAA9B,CAEF,OAAO,SAAQ,CAAClE,CAAD,CAAO,CACpB,IAAIf;AAAM,EACVc,EAAA,CAAWC,CAAX,CAAiBd,CAAA,CAAmBD,CAAnB,CAAwB,QAAQ,CAACmG,CAAD,CAAMpC,CAAN,CAAe,CAC9D,MAAO,CAAC,UAAAqC,KAAA,CAAgBF,CAAA,CAAcC,CAAd,CAAmBpC,CAAnB,CAAhB,CADsD,CAA/C,CAAjB,CAGA,OAAO/D,EAAAI,KAAA,CAAS,EAAT,CALa,CAJ8B,CAA1C,CA4CZ,KAAAiG,UAAA,CAAiBC,QAAQ,CAACD,CAAD,CAAY,CACnC,MAAIxG,EAAA0G,UAAA,CAAkBF,CAAlB,CAAJ,EACEL,CACO,CADMK,CACN,CAAA,IAFT,EAISL,CAL0B,CA/CV,CAoX7B,CAmIAnG,EAAAgG,OAAA,CAAe,YAAf,CAAAW,OAAA,CAAoC,OAApC,CAA6C,CAAC,WAAD,CAAc,QAAQ,CAACC,CAAD,CAAY,CAAA,IACzEC,EACE,yFAFuE,CAGzEC,EAAgB,WAHyD,CAKzEC,EAAc/G,CAAA6E,SAAA,CAAiB,OAAjB,CAL2D,CAMzEmC,EAAWhH,CAAAgH,SAEf,OAAO,SAAQ,CAACC,CAAD,CAAOC,CAAP,CAAe/E,CAAf,CAA2B,CAwBxCgF,QAASA,EAAO,CAACF,CAAD,CAAO,CAChBA,CAAL,EAGA/F,CAAAyC,KAAA,CAAU1D,CAAA,CAAagH,CAAb,CAAV,CAJqB,CAOvBG,QAASA,EAAO,CAACC,CAAD,CAAMJ,CAAN,CAAY,CAC1B,IAAIjD,CACJ9C,EAAAyC,KAAA,CAAU,KAAV,CACI3D,EAAAsH,WAAA,CAAmBnF,CAAnB,CAAJ,GACEA,CADF,CACeA,CAAA,CAAWkF,CAAX,CADf,CAGA,IAAIrH,CAAAuH,SAAA,CAAiBpF,CAAjB,CAAJ,CACE,IAAK6B,CAAL,GAAY7B,EAAZ,CACEjB,CAAAyC,KAAA,CAAUK,CAAV;AAAgB,IAAhB,CAAuB7B,CAAA,CAAW6B,CAAX,CAAvB,CAAyC,IAAzC,CAFJ,KAKE7B,EAAA,CAAa,EAEX,EAAAnC,CAAA0G,UAAA,CAAkBQ,CAAlB,CAAJ,EAAmC,QAAnC,EAA+C/E,EAA/C,EACEjB,CAAAyC,KAAA,CAAU,UAAV,CACUuD,CADV,CAEU,IAFV,CAIFhG,EAAAyC,KAAA,CAAU,QAAV,CACU0D,CAAApE,QAAA,CAAY,IAAZ,CAAkB,QAAlB,CADV,CAEU,IAFV,CAGAkE,EAAA,CAAQF,CAAR,CACA/F,EAAAyC,KAAA,CAAU,MAAV,CAtB0B,CA9B5B,GAAY,IAAZ,EAAIsD,CAAJ,EAA6B,EAA7B,GAAoBA,CAApB,CAAiC,MAAOA,EACxC,IAAK,CAAAD,CAAA,CAASC,CAAT,CAAL,CAAqB,KAAMF,EAAA,CAAY,WAAZ,CAA8DE,CAA9D,CAAN,CAOrB,IAJA,IAAIO,EAAMP,CAAV,CACI/F,EAAO,EADX,CAEImG,CAFJ,CAGIvG,CACJ,CAAQ2G,CAAR,CAAgBD,CAAAC,MAAA,CAAUZ,CAAV,CAAhB,CAAA,CAEEQ,CAQA,CARMI,CAAA,CAAM,CAAN,CAQN,CANKA,CAAA,CAAM,CAAN,CAML,EANkBA,CAAA,CAAM,CAAN,CAMlB,GALEJ,CAKF,EALSI,CAAA,CAAM,CAAN,CAAA,CAAW,SAAX,CAAuB,SAKhC,EAL6CJ,CAK7C,EAHAvG,CAGA,CAHI2G,CAAAC,MAGJ,CAFAP,CAAA,CAAQK,CAAAG,OAAA,CAAW,CAAX,CAAc7G,CAAd,CAAR,CAEA,CADAsG,CAAA,CAAQC,CAAR,CAAaI,CAAA,CAAM,CAAN,CAAAxE,QAAA,CAAiB6D,CAAjB,CAAgC,EAAhC,CAAb,CACA,CAAAU,CAAA,CAAMA,CAAAI,UAAA,CAAc9G,CAAd,CAAkB2G,CAAA,CAAM,CAAN,CAAA1G,OAAlB,CAERoG,EAAA,CAAQK,CAAR,CACA,OAAOZ,EAAA,CAAU1F,CAAAX,KAAA,CAAU,EAAV,CAAV,CAtBiC,CARmC,CAAlC,CAA7C,CApoB2B,CAA1B,CAAD,CAusBGR,MAvsBH,CAusBWA,MAAAC,QAvsBX;", "sources":["angular-sanitize.js"], -"names":["window","angular","undefined","sanitizeText","chars","buf","htmlSanitizeWriter","writer","noop","join","makeMap","str","obj","items","split","i","length","htmlParser","html","handler","parseStartTag","tag","tagName","rest","unary","lowercase","blockElements","stack","last","inlineElements","parseEndTag","optionalEndTagElements","voidElements","push","attrs","replace","ATTR_REGEXP","match","name","doubleQuotedValue","singleQuotedValue","unquotedValue","decodeEntities","start","pos","end","index","text","stack.last","specialElements","RegExp","all","COMMENT_REGEXP","CDATA_REGEXP","indexOf","lastIndexOf","comment","substring","DOCTYPE_REGEXP","test","BEGING_END_TAGE_REGEXP","END_TAG_REGEXP","BEGIN_TAG_REGEXP","START_TAG_REGEXP","$sanitizeMinErr","value","parts","spaceRe","exec","spaceBefore","spaceAfter","content","hiddenPre","innerHTML","textContent","innerText","encodeEntities","SURROGATE_PAIR_REGEXP","hi","charCodeAt","low","NON_ALPHANUMERIC_REGEXP","uriValidator","ignore","out","bind","validElements","forEach","key","lkey","isImage","validAttrs","uriAttrs","$$minErr","optionalEndTagBlockElements","optionalEndTagInlineElements","extend","document","createElement","module","provider","$SanitizeProvider","$get","$$sanitizeUri","uri","filter","$sanitize","LINKY_URL_REGEXP","MAILTO_REGEXP","target","addText","addLink","url","isDefined","raw","substr"] +"names":["window","angular","sanitizeText","chars","buf","htmlSanitizeWriter","writer","noop","join","toMap","str","lowercaseKeys","obj","items","split","i","length","lowercase","htmlParser","html","handler","undefined","inertBodyElement","innerHTML","mXSSAttempts","$sanitizeMinErr","document","documentMode","stripCustomNsAttrs","node","firstChild","nodeType","start","nodeName","toLowerCase","attrToMap","attributes","textContent","nextNode","end","nextSibling","parentNode","removeChild","attrs","map","ii","attr","name","value","encodeEntities","replace","SURROGATE_PAIR_REGEXP","hi","charCodeAt","low","NON_ALPHANUMERIC_REGEXP","uriValidator","ignoreCurrentElement","out","bind","push","tag","blockedElements","validElements","forEach","key","lkey","isImage","validAttrs","uriAttrs","voidElements","Node","ELEMENT_NODE","l","attrNode","attrName","indexOf","removeAttributeNode","$$minErr","optionalEndTagBlockElements","optionalEndTagInlineElements","optionalEndTagElements","extend","blockElements","inlineElements","svgElements","htmlAttrs","svgAttrs","implementation","doc","createHTMLDocument","bodyElements","getElementsByTagName","documentElement","getDocumentElement","createElement","appendChild","module","provider","$SanitizeProvider","svgEnabled","$get","$$sanitizeUri","uri","test","enableSvg","this.enableSvg","isDefined","filter","$sanitize","LINKY_URL_REGEXP","MAILTO_REGEXP","linkyMinErr","isString","text","target","addText","addLink","url","isFunction","isObject","raw","match","index","substr","substring"] } diff --git a/app/bower_components/angular-sanitize/bower.json b/app/bower_components/angular-sanitize/bower.json index 7daf7d9..ae9664b 100644 --- a/app/bower_components/angular-sanitize/bower.json +++ b/app/bower_components/angular-sanitize/bower.json @@ -1,8 +1,10 @@ { "name": "angular-sanitize", - "version": "1.2.21", + "version": "1.5.6-build.4801+sha.3b5751d", + "license": "MIT", "main": "./angular-sanitize.js", + "ignore": [], "dependencies": { - "angular": "1.2.21" + "angular": "1.5.6-build.4801+sha.3b5751d" } } diff --git a/app/bower_components/angular-sanitize/index.js b/app/bower_components/angular-sanitize/index.js new file mode 100644 index 0000000..dd5d22e --- /dev/null +++ b/app/bower_components/angular-sanitize/index.js @@ -0,0 +1,2 @@ +require('./angular-sanitize'); +module.exports = 'ngSanitize'; diff --git a/app/bower_components/angular-sanitize/package.json b/app/bower_components/angular-sanitize/package.json new file mode 100644 index 0000000..319e2a4 --- /dev/null +++ b/app/bower_components/angular-sanitize/package.json @@ -0,0 +1,26 @@ +{ + "name": "angular-sanitize", + "version": "1.5.6-build.4801+sha.3b5751d", + "description": "AngularJS module for sanitizing HTML", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "repository": { + "type": "git", + "url": "https://github.com/angular/angular.js.git" + }, + "keywords": [ + "angular", + "framework", + "browser", + "html", + "client-side" + ], + "author": "Angular Core Team ", + "license": "MIT", + "bugs": { + "url": "https://github.com/angular/angular.js/issues" + }, + "homepage": "http://angularjs.org" +} diff --git a/app/bower_components/angular-scenario/.bower.json b/app/bower_components/angular-scenario/.bower.json index 89f1aa4..fb7439b 100644 --- a/app/bower_components/angular-scenario/.bower.json +++ b/app/bower_components/angular-scenario/.bower.json @@ -1,18 +1,20 @@ { "name": "angular-scenario", - "version": "1.2.21", + "version": "1.5.6-build.4801+sha.3b5751d", + "license": "MIT", "main": "./angular-scenario.js", + "ignore": [], "dependencies": { - "angular": "1.2.21" + "angular": "1.5.6-build.4801+sha.3b5751d" }, "homepage": "https://github.com/angular/bower-angular-scenario", - "_release": "1.2.21", + "_release": "1.5.6-build.4801+sha.3b5751d", "_resolution": { "type": "version", - "tag": "v1.2.21", - "commit": "1e442f99fe1531084a8c502b0acb097f7f65988b" + "tag": "v1.5.6-build.4801+sha.3b5751d", + "commit": "61cc49c61a92ae750c7d5a238373df1db3b40305" }, - "_source": "git://github.com/angular/bower-angular-scenario.git", - "_target": "1.2.21", + "_source": "https://github.com/angular/bower-angular-scenario.git", + "_target": "~1.5.6", "_originalSource": "angular-scenario" } \ No newline at end of file diff --git a/app/bower_components/angular-scenario/README.md b/app/bower_components/angular-scenario/README.md index 7f80a8c..a4a0053 100644 --- a/app/bower_components/angular-scenario/README.md +++ b/app/bower_components/angular-scenario/README.md @@ -1,17 +1,36 @@ -# bower-angular-scenario +# packaged angular-scenario -This repo is for distribution on `bower`. The source for this module is in the +This tool is now in maintenance mode. If you are starting a new project, please use +[Protractor](https://github.com/angular/protractor). Existing projects using scenario runner are +advised to migrate to protractor, as this tool is unlikely to receive updates. + +This repo is for distribution on `npm` and `bower`. The source for this module is in the [main AngularJS repo](https://github.com/angular/angular.js/tree/master/src/ngScenario). Please file issues and pull requests against that repo. ## Install -Install with `bower`: +You can install this package either with `npm` or with `bower`. + +### npm + +```shell +npm install angular-scenario +``` + +The files are then available at `node_modules/angular-scenario/`. + +Note that this package is not in CommonJS format, so doing `require('angular-scenario')` will +return `undefined`. + +### bower ```shell bower install angular-scenario ``` +The files are then available at `bower_components/angular-scenario/`. + ## Documentation Documentation is available on the @@ -21,7 +40,7 @@ Documentation is available on the The MIT License -Copyright (c) 2010-2012 Google, Inc. http://angularjs.org +Copyright (c) 2010-2015 Google, Inc. http://angularjs.org Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/app/bower_components/angular-scenario/angular-scenario.js b/app/bower_components/angular-scenario/angular-scenario.js index 5a6ac19..158a511 100644 --- a/app/bower_components/angular-scenario/angular-scenario.js +++ b/app/bower_components/angular-scenario/angular-scenario.js @@ -1,92 +1,85 @@ /*! - * jQuery JavaScript Library v1.10.2 + * jQuery JavaScript Library v2.2.3 * http://jquery.com/ * * Includes Sizzle.js * http://sizzlejs.com/ * - * Copyright 2005, 2013 jQuery Foundation, Inc. and other contributors + * Copyright jQuery Foundation and other contributors * Released under the MIT license * http://jquery.org/license * - * Date: 2013-07-03T13:48Z + * Date: 2016-04-05T19:26Z */ -(function( window, undefined ) {'use strict'; -// Can't do this because several apps including ASP.NET trace +(function( global, factory ) {'use strict'; + + if ( typeof module === "object" && typeof module.exports === "object" ) { + // For CommonJS and CommonJS-like environments where a proper `window` + // is present, execute the factory and get jQuery. + // For environments that do not have a `window` with a `document` + // (such as Node.js), expose a factory as module.exports. + // This accentuates the need for the creation of a real `window`. + // e.g. var jQuery = require("jquery")(window); + // See ticket #14549 for more info. + module.exports = global.document ? + factory( global, true ) : + function( w ) { + if ( !w.document ) { + throw new Error( "jQuery requires a window with a document" ); + } + return factory( w ); + }; + } else { + factory( global ); + } + +// Pass this if window is not defined yet +}(typeof window !== "undefined" ? window : this, function( window, noGlobal ) { + +// Support: Firefox 18+ +// Can't be in strict mode, several libs including ASP.NET trace // the stack via arguments.caller.callee and Firefox dies if // you try to trace through "use strict" call chains. (#13335) -// Support: Firefox 18+ // +var arr = []; + +var document = window.document; + +var slice = arr.slice; + +var concat = arr.concat; + +var push = arr.push; + +var indexOf = arr.indexOf; + +var class2type = {}; + +var toString = class2type.toString; + +var hasOwn = class2type.hasOwnProperty; + +var support = {}; + + + var - // The deferred used on DOM ready - readyList, - - // A central reference to the root jQuery(document) - rootjQuery, - - // Support: IE<10 - // For `typeof xmlNode.method` instead of `xmlNode.method !== undefined` - core_strundefined = typeof undefined, - - // Use the correct document accordingly with window argument (sandbox) - location = window.location, - document = window.document, - docElem = document.documentElement, - - // Map over jQuery in case of overwrite - _jQuery = window.jQuery, - - // Map over the $ in case of overwrite - _$ = window.$, - - // [[Class]] -> type pairs - class2type = {}, - - // List of deleted data cache ids, so we can reuse them - core_deletedIds = [], - - core_version = "1.10.2", - - // Save a reference to some core methods - core_concat = core_deletedIds.concat, - core_push = core_deletedIds.push, - core_slice = core_deletedIds.slice, - core_indexOf = core_deletedIds.indexOf, - core_toString = class2type.toString, - core_hasOwn = class2type.hasOwnProperty, - core_trim = core_version.trim, + version = "2.2.3", // Define a local copy of jQuery jQuery = function( selector, context ) { + // The jQuery object is actually just the init constructor 'enhanced' - return new jQuery.fn.init( selector, context, rootjQuery ); + // Need init if jQuery is called (just allow error to be thrown if not included) + return new jQuery.fn.init( selector, context ); }, - // Used for matching numbers - core_pnum = /[+-]?(?:\d*\.|)\d+(?:[eE][+-]?\d+|)/.source, - - // Used for splitting on whitespace - core_rnotwhite = /\S+/g, - - // Make sure we trim BOM and NBSP (here's looking at you, Safari 5.0 and IE) + // Support: Android<4.1 + // Make sure we trim BOM and NBSP rtrim = /^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, - // A simple way to check for HTML strings - // Prioritize #id over to avoid XSS via location.hash (#9521) - // Strict HTML recognition (#11290: must start with <) - rquickExpr = /^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]*))$/, - - // Match a standalone tag - rsingleTag = /^<(\w+)\s*\/?>(?:<\/\1>|)$/, - - // JSON RegExp - rvalidchars = /^[\],:{}\s]*$/, - rvalidbraces = /(?:^|:|,)(?:\s*\[)+/g, - rvalidescape = /\\(?:["\\\/bfnrt]|u[\da-fA-F]{4})/g, - rvalidtokens = /"[^"\\\r\n]*"|true|false|null|-?(?:\d+\.|)\d+(?:[eE][+-]?\d+|)/g, - // Matches dashed string for camelizing rmsPrefix = /^-ms-/, rdashAlpha = /-([\da-z])/gi, @@ -94,134 +87,14 @@ var // Used by jQuery.camelCase as callback to replace() fcamelCase = function( all, letter ) { return letter.toUpperCase(); - }, - - // The ready event handler - completed = function( event ) { - - // readyState === "complete" is good enough for us to call the dom ready in oldIE - if ( document.addEventListener || event.type === "load" || document.readyState === "complete" ) { - detach(); - jQuery.ready(); - } - }, - // Clean-up method for dom ready events - detach = function() { - if ( document.addEventListener ) { - document.removeEventListener( "DOMContentLoaded", completed, false ); - window.removeEventListener( "load", completed, false ); - - } else { - document.detachEvent( "onreadystatechange", completed ); - window.detachEvent( "onload", completed ); - } }; jQuery.fn = jQuery.prototype = { + // The current version of jQuery being used - jquery: core_version, + jquery: version, constructor: jQuery, - init: function( selector, context, rootjQuery ) { - var match, elem; - - // HANDLE: $(""), $(null), $(undefined), $(false) - if ( !selector ) { - return this; - } - - // Handle HTML strings - if ( typeof selector === "string" ) { - if ( selector.charAt(0) === "<" && selector.charAt( selector.length - 1 ) === ">" && selector.length >= 3 ) { - // Assume that strings that start and end with <> are HTML and skip the regex check - match = [ null, selector, null ]; - - } else { - match = rquickExpr.exec( selector ); - } - - // Match html or make sure no context is specified for #id - if ( match && (match[1] || !context) ) { - - // HANDLE: $(html) -> $(array) - if ( match[1] ) { - context = context instanceof jQuery ? context[0] : context; - - // scripts is true for back-compat - jQuery.merge( this, jQuery.parseHTML( - match[1], - context && context.nodeType ? context.ownerDocument || context : document, - true - ) ); - - // HANDLE: $(html, props) - if ( rsingleTag.test( match[1] ) && jQuery.isPlainObject( context ) ) { - for ( match in context ) { - // Properties of context are called as methods if possible - if ( jQuery.isFunction( this[ match ] ) ) { - this[ match ]( context[ match ] ); - - // ...and otherwise set as attributes - } else { - this.attr( match, context[ match ] ); - } - } - } - - return this; - - // HANDLE: $(#id) - } else { - elem = document.getElementById( match[2] ); - - // Check parentNode to catch when Blackberry 4.6 returns - // nodes that are no longer in the document #6963 - if ( elem && elem.parentNode ) { - // Handle the case where IE and Opera return items - // by name instead of ID - if ( elem.id !== match[2] ) { - return rootjQuery.find( selector ); - } - - // Otherwise, we inject the element directly into the jQuery object - this.length = 1; - this[0] = elem; - } - - this.context = document; - this.selector = selector; - return this; - } - - // HANDLE: $(expr, $(...)) - } else if ( !context || context.jquery ) { - return ( context || rootjQuery ).find( selector ); - - // HANDLE: $(expr, context) - // (which is just equivalent to: $(context).find(expr) - } else { - return this.constructor( context ).find( selector ); - } - - // HANDLE: $(DOMElement) - } else if ( selector.nodeType ) { - this.context = this[0] = selector; - this.length = 1; - return this; - - // HANDLE: $(function) - // Shortcut for document ready - } else if ( jQuery.isFunction( selector ) ) { - return rootjQuery.ready( selector ); - } - - if ( selector.selector !== undefined ) { - this.selector = selector.selector; - this.context = selector.context; - } - - return jQuery.makeArray( selector, this ); - }, // Start with an empty selector selector: "", @@ -230,19 +103,19 @@ jQuery.fn = jQuery.prototype = { length: 0, toArray: function() { - return core_slice.call( this ); + return slice.call( this ); }, // Get the Nth element in the matched element set OR // Get the whole matched element set as a clean array get: function( num ) { - return num == null ? + return num != null ? - // Return a 'clean' array - this.toArray() : + // Return just the one element from the set + ( num < 0 ? this[ num + this.length ] : this[ num ] ) : - // Return just the object - ( num < 0 ? this[ this.length + num ] : this[ num ] ); + // Return all the elements in a clean array + slice.call( this ); }, // Take an array of elements and push it onto the stack @@ -261,21 +134,18 @@ jQuery.fn = jQuery.prototype = { }, // Execute a callback for every element in the matched set. - // (You can seed the arguments with an array of args, but this is - // only used internally.) - each: function( callback, args ) { - return jQuery.each( this, callback, args ); + each: function( callback ) { + return jQuery.each( this, callback ); }, - ready: function( fn ) { - // Add the callback - jQuery.ready.promise().done( fn ); - - return this; + map: function( callback ) { + return this.pushStack( jQuery.map( this, function( elem, i ) { + return callback.call( elem, i, elem ); + } ) ); }, slice: function() { - return this.pushStack( core_slice.apply( this, arguments ) ); + return this.pushStack( slice.apply( this, arguments ) ); }, first: function() { @@ -289,32 +159,23 @@ jQuery.fn = jQuery.prototype = { eq: function( i ) { var len = this.length, j = +i + ( i < 0 ? len : 0 ); - return this.pushStack( j >= 0 && j < len ? [ this[j] ] : [] ); - }, - - map: function( callback ) { - return this.pushStack( jQuery.map(this, function( elem, i ) { - return callback.call( elem, i, elem ); - })); + return this.pushStack( j >= 0 && j < len ? [ this[ j ] ] : [] ); }, end: function() { - return this.prevObject || this.constructor(null); + return this.prevObject || this.constructor(); }, // For internal use only. // Behaves like an Array's method, not like a jQuery method. - push: core_push, - sort: [].sort, - splice: [].splice + push: push, + sort: arr.sort, + splice: arr.splice }; -// Give the init function the jQuery prototype for later instantiation -jQuery.fn.init.prototype = jQuery.fn; - jQuery.extend = jQuery.fn.extend = function() { - var src, copyIsArray, copy, name, options, clone, - target = arguments[0] || {}, + var options, name, src, copy, copyIsArray, clone, + target = arguments[ 0 ] || {}, i = 1, length = arguments.length, deep = false; @@ -322,25 +183,28 @@ jQuery.extend = jQuery.fn.extend = function() { // Handle a deep copy situation if ( typeof target === "boolean" ) { deep = target; - target = arguments[1] || {}; - // skip the boolean and the target - i = 2; + + // Skip the boolean and the target + target = arguments[ i ] || {}; + i++; } // Handle case when target is a string or something (possible in deep copy) - if ( typeof target !== "object" && !jQuery.isFunction(target) ) { + if ( typeof target !== "object" && !jQuery.isFunction( target ) ) { target = {}; } - // extend jQuery itself if only one argument is passed - if ( length === i ) { + // Extend jQuery itself if only one argument is passed + if ( i === length ) { target = this; - --i; + i--; } for ( ; i < length; i++ ) { + // Only deal with non-null/undefined values - if ( (options = arguments[ i ]) != null ) { + if ( ( options = arguments[ i ] ) != null ) { + // Extend the base object for ( name in options ) { src = target[ name ]; @@ -352,13 +216,15 @@ jQuery.extend = jQuery.fn.extend = function() { } // Recurse if we're merging plain objects or arrays - if ( deep && copy && ( jQuery.isPlainObject(copy) || (copyIsArray = jQuery.isArray(copy)) ) ) { + if ( deep && copy && ( jQuery.isPlainObject( copy ) || + ( copyIsArray = jQuery.isArray( copy ) ) ) ) { + if ( copyIsArray ) { copyIsArray = false; - clone = src && jQuery.isArray(src) ? src : []; + clone = src && jQuery.isArray( src ) ? src : []; } else { - clone = src && jQuery.isPlainObject(src) ? src : {}; + clone = src && jQuery.isPlainObject( src ) ? src : {}; } // Never move original objects, clone them @@ -376,133 +242,63 @@ jQuery.extend = jQuery.fn.extend = function() { return target; }; -jQuery.extend({ +jQuery.extend( { + // Unique for each copy of jQuery on the page - // Non-digits removed to match rinlinejQuery - expando: "jQuery" + ( core_version + Math.random() ).replace( /\D/g, "" ), + expando: "jQuery" + ( version + Math.random() ).replace( /\D/g, "" ), - noConflict: function( deep ) { - if ( window.$ === jQuery ) { - window.$ = _$; - } + // Assume jQuery is ready without the ready module + isReady: true, - if ( deep && window.jQuery === jQuery ) { - window.jQuery = _jQuery; - } - - return jQuery; + error: function( msg ) { + throw new Error( msg ); }, - // Is the DOM ready to be used? Set to true once it occurs. - isReady: false, + noop: function() {}, - // A counter to track how many items to wait for before - // the ready event fires. See #6781 - readyWait: 1, - - // Hold (or release) the ready event - holdReady: function( hold ) { - if ( hold ) { - jQuery.readyWait++; - } else { - jQuery.ready( true ); - } - }, - - // Handle when the DOM is ready - ready: function( wait ) { - - // Abort if there are pending holds or we're already ready - if ( wait === true ? --jQuery.readyWait : jQuery.isReady ) { - return; - } - - // Make sure body exists, at least, in case IE gets a little overzealous (ticket #5443). - if ( !document.body ) { - return setTimeout( jQuery.ready ); - } - - // Remember that the DOM is ready - jQuery.isReady = true; - - // If a normal DOM Ready event fired, decrement, and wait if need be - if ( wait !== true && --jQuery.readyWait > 0 ) { - return; - } - - // If there are functions bound, to execute - readyList.resolveWith( document, [ jQuery ] ); - - // Trigger any bound ready events - if ( jQuery.fn.trigger ) { - jQuery( document ).trigger("ready").off("ready"); - } - }, - - // See test/unit/core.js for details concerning isFunction. - // Since version 1.3, DOM methods and functions like alert - // aren't supported. They return false on IE (#2968). isFunction: function( obj ) { - return jQuery.type(obj) === "function"; + return jQuery.type( obj ) === "function"; }, - isArray: Array.isArray || function( obj ) { - return jQuery.type(obj) === "array"; - }, + isArray: Array.isArray, isWindow: function( obj ) { - /* jshint eqeqeq: false */ - return obj != null && obj == obj.window; + return obj != null && obj === obj.window; }, isNumeric: function( obj ) { - return !isNaN( parseFloat(obj) ) && isFinite( obj ); - }, - type: function( obj ) { - if ( obj == null ) { - return String( obj ); - } - return typeof obj === "object" || typeof obj === "function" ? - class2type[ core_toString.call(obj) ] || "object" : - typeof obj; + // parseFloat NaNs numeric-cast false positives (null|true|false|"") + // ...but misinterprets leading-number strings, particularly hex literals ("0x...") + // subtraction forces infinities to NaN + // adding 1 corrects loss of precision from parseFloat (#15100) + var realStringObj = obj && obj.toString(); + return !jQuery.isArray( obj ) && ( realStringObj - parseFloat( realStringObj ) + 1 ) >= 0; }, isPlainObject: function( obj ) { var key; - // Must be an Object. - // Because of IE, we also have to check the presence of the constructor property. - // Make sure that DOM nodes and window objects don't pass through, as well - if ( !obj || jQuery.type(obj) !== "object" || obj.nodeType || jQuery.isWindow( obj ) ) { + // Not plain objects: + // - Any object or value whose internal [[Class]] property is not "[object Object]" + // - DOM nodes + // - window + if ( jQuery.type( obj ) !== "object" || obj.nodeType || jQuery.isWindow( obj ) ) { return false; } - try { - // Not own constructor property must be Object - if ( obj.constructor && - !core_hasOwn.call(obj, "constructor") && - !core_hasOwn.call(obj.constructor.prototype, "isPrototypeOf") ) { - return false; - } - } catch ( e ) { - // IE8,9 Will throw exceptions on certain host objects #9897 + // Not own constructor property must be Object + if ( obj.constructor && + !hasOwn.call( obj, "constructor" ) && + !hasOwn.call( obj.constructor.prototype || {}, "isPrototypeOf" ) ) { return false; } - // Support: IE<9 - // Handle iteration over inherited properties before own properties. - if ( jQuery.support.ownLast ) { - for ( key in obj ) { - return core_hasOwn.call( obj, key ); - } - } - // Own properties are enumerated firstly, so to speed up, - // if last one is own, then all properties are own. + // if last one is own, then all properties are own for ( key in obj ) {} - return key === undefined || core_hasOwn.call( obj, key ); + return key === undefined || hasOwn.call( obj, key ); }, isEmptyObject: function( obj ) { @@ -513,109 +309,45 @@ jQuery.extend({ return true; }, - error: function( msg ) { - throw new Error( msg ); + type: function( obj ) { + if ( obj == null ) { + return obj + ""; + } + + // Support: Android<4.0, iOS<6 (functionish RegExp) + return typeof obj === "object" || typeof obj === "function" ? + class2type[ toString.call( obj ) ] || "object" : + typeof obj; }, - // data: string of html - // context (optional): If specified, the fragment will be created in this context, defaults to document - // keepScripts (optional): If true, will include scripts passed in the html string - parseHTML: function( data, context, keepScripts ) { - if ( !data || typeof data !== "string" ) { - return null; - } - if ( typeof context === "boolean" ) { - keepScripts = context; - context = false; - } - context = context || document; - - var parsed = rsingleTag.exec( data ), - scripts = !keepScripts && []; - - // Single tag - if ( parsed ) { - return [ context.createElement( parsed[1] ) ]; - } - - parsed = jQuery.buildFragment( [ data ], context, scripts ); - if ( scripts ) { - jQuery( scripts ).remove(); - } - return jQuery.merge( [], parsed.childNodes ); - }, - - parseJSON: function( data ) { - // Attempt to parse using the native JSON parser first - if ( window.JSON && window.JSON.parse ) { - return window.JSON.parse( data ); - } - - if ( data === null ) { - return data; - } - - if ( typeof data === "string" ) { - - // Make sure leading/trailing whitespace is removed (IE can't handle it) - data = jQuery.trim( data ); - - if ( data ) { - // Make sure the incoming data is actual JSON - // Logic borrowed from http://json.org/json2.js - if ( rvalidchars.test( data.replace( rvalidescape, "@" ) - .replace( rvalidtokens, "]" ) - .replace( rvalidbraces, "")) ) { - - return ( new Function( "return " + data ) )(); - } - } - } - - jQuery.error( "Invalid JSON: " + data ); - }, - - // Cross-browser xml parsing - parseXML: function( data ) { - var xml, tmp; - if ( !data || typeof data !== "string" ) { - return null; - } - try { - if ( window.DOMParser ) { // Standard - tmp = new DOMParser(); - xml = tmp.parseFromString( data , "text/xml" ); - } else { // IE - xml = new ActiveXObject( "Microsoft.XMLDOM" ); - xml.async = "false"; - xml.loadXML( data ); - } - } catch( e ) { - xml = undefined; - } - if ( !xml || !xml.documentElement || xml.getElementsByTagName( "parsererror" ).length ) { - jQuery.error( "Invalid XML: " + data ); - } - return xml; - }, - - noop: function() {}, - // Evaluates a script in a global context - // Workarounds based on findings by Jim Driscoll - // http://weblogs.java.net/blog/driscoll/archive/2009/09/08/eval-javascript-global-context - globalEval: function( data ) { - if ( data && jQuery.trim( data ) ) { - // We use execScript on Internet Explorer - // We use an anonymous function so that context is window - // rather than jQuery in Firefox - ( window.execScript || function( data ) { - window[ "eval" ].call( window, data ); - } )( data ); + globalEval: function( code ) { + var script, + indirect = eval; + + code = jQuery.trim( code ); + + if ( code ) { + + // If the code includes a valid, prologue position + // strict mode pragma, execute code by injecting a + // script tag into the document. + if ( code.indexOf( "use strict" ) === 1 ) { + script = document.createElement( "script" ); + script.text = code; + document.head.appendChild( script ).parentNode.removeChild( script ); + } else { + + // Otherwise, avoid the DOM node creation, insertion + // and removal by using an indirect global eval + + indirect( code ); + } } }, // Convert dashed to camelCase; used by the css and data modules + // Support: IE9-11+ // Microsoft forgot to hump their vendor prefix (#9572) camelCase: function( string ) { return string.replace( rmsPrefix, "ms-" ).replace( rdashAlpha, fcamelCase ); @@ -625,49 +357,20 @@ jQuery.extend({ return elem.nodeName && elem.nodeName.toLowerCase() === name.toLowerCase(); }, - // args is for internal usage only - each: function( obj, callback, args ) { - var value, - i = 0, - length = obj.length, - isArray = isArraylike( obj ); + each: function( obj, callback ) { + var length, i = 0; - if ( args ) { - if ( isArray ) { - for ( ; i < length; i++ ) { - value = callback.apply( obj[ i ], args ); - - if ( value === false ) { - break; - } - } - } else { - for ( i in obj ) { - value = callback.apply( obj[ i ], args ); - - if ( value === false ) { - break; - } + if ( isArrayLike( obj ) ) { + length = obj.length; + for ( ; i < length; i++ ) { + if ( callback.call( obj[ i ], i, obj[ i ] ) === false ) { + break; } } - - // A special, fast, case for the most common use of each } else { - if ( isArray ) { - for ( ; i < length; i++ ) { - value = callback.call( obj[ i ], i, obj[ i ] ); - - if ( value === false ) { - break; - } - } - } else { - for ( i in obj ) { - value = callback.call( obj[ i ], i, obj[ i ] ); - - if ( value === false ) { - break; - } + for ( i in obj ) { + if ( callback.call( obj[ i ], i, obj[ i ] ) === false ) { + break; } } } @@ -675,33 +378,25 @@ jQuery.extend({ return obj; }, - // Use native String.trim function wherever possible - trim: core_trim && !core_trim.call("\uFEFF\xA0") ? - function( text ) { - return text == null ? - "" : - core_trim.call( text ); - } : - - // Otherwise use our own trimming functionality - function( text ) { - return text == null ? - "" : - ( text + "" ).replace( rtrim, "" ); - }, + // Support: Android<4.1 + trim: function( text ) { + return text == null ? + "" : + ( text + "" ).replace( rtrim, "" ); + }, // results is for internal usage only makeArray: function( arr, results ) { var ret = results || []; if ( arr != null ) { - if ( isArraylike( Object(arr) ) ) { + if ( isArrayLike( Object( arr ) ) ) { jQuery.merge( ret, typeof arr === "string" ? [ arr ] : arr ); } else { - core_push.call( ret, arr ); + push.call( ret, arr ); } } @@ -709,40 +404,16 @@ jQuery.extend({ }, inArray: function( elem, arr, i ) { - var len; - - if ( arr ) { - if ( core_indexOf ) { - return core_indexOf.call( arr, elem, i ); - } - - len = arr.length; - i = i ? i < 0 ? Math.max( 0, len + i ) : i : 0; - - for ( ; i < len; i++ ) { - // Skip accessing in sparse arrays - if ( i in arr && arr[ i ] === elem ) { - return i; - } - } - } - - return -1; + return arr == null ? -1 : indexOf.call( arr, elem, i ); }, merge: function( first, second ) { - var l = second.length, - i = first.length, - j = 0; + var len = +second.length, + j = 0, + i = first.length; - if ( typeof l === "number" ) { - for ( ; j < l; j++ ) { - first[ i++ ] = second[ j ]; - } - } else { - while ( second[j] !== undefined ) { - first[ i++ ] = second[ j++ ]; - } + for ( ; j < len; j++ ) { + first[ i++ ] = second[ j ]; } first.length = i; @@ -750,40 +421,39 @@ jQuery.extend({ return first; }, - grep: function( elems, callback, inv ) { - var retVal, - ret = [], + grep: function( elems, callback, invert ) { + var callbackInverse, + matches = [], i = 0, - length = elems.length; - inv = !!inv; + length = elems.length, + callbackExpect = !invert; // Go through the array, only saving the items // that pass the validator function for ( ; i < length; i++ ) { - retVal = !!callback( elems[ i ], i ); - if ( inv !== retVal ) { - ret.push( elems[ i ] ); + callbackInverse = !callback( elems[ i ], i ); + if ( callbackInverse !== callbackExpect ) { + matches.push( elems[ i ] ); } } - return ret; + return matches; }, // arg is for internal usage only map: function( elems, callback, arg ) { - var value, + var length, value, i = 0, - length = elems.length, - isArray = isArraylike( elems ), ret = []; - // Go through the array, translating each of the items to their - if ( isArray ) { + // Go through the array, translating each of the items to their new values + if ( isArrayLike( elems ) ) { + length = elems.length; for ( ; i < length; i++ ) { value = callback( elems[ i ], i, arg ); if ( value != null ) { - ret[ ret.length ] = value; + ret.push( value ); } } @@ -793,13 +463,13 @@ jQuery.extend({ value = callback( elems[ i ], i, arg ); if ( value != null ) { - ret[ ret.length ] = value; + ret.push( value ); } } } // Flatten any nested arrays - return core_concat.apply( [], ret ); + return concat.apply( [], ret ); }, // A global GUID counter for objects @@ -808,7 +478,7 @@ jQuery.extend({ // Bind a function to a context, optionally partially applying any // arguments. proxy: function( fn, context ) { - var args, proxy, tmp; + var tmp, args, proxy; if ( typeof context === "string" ) { tmp = fn[ context ]; @@ -823,9 +493,9 @@ jQuery.extend({ } // Simulated bind - args = core_slice.call( arguments, 2 ); + args = slice.call( arguments, 2 ); proxy = function() { - return fn.apply( context || this, args.concat( core_slice.call( arguments ) ) ); + return fn.apply( context || this, args.concat( slice.call( arguments ) ) ); }; // Set the guid of unique handler to the same of original handler, so it can be removed @@ -834,193 +504,69 @@ jQuery.extend({ return proxy; }, - // Multifunctional method to get and set values of a collection - // The value/s can optionally be executed if it's a function - access: function( elems, fn, key, value, chainable, emptyGet, raw ) { - var i = 0, - length = elems.length, - bulk = key == null; + now: Date.now, - // Sets many values - if ( jQuery.type( key ) === "object" ) { - chainable = true; - for ( i in key ) { - jQuery.access( elems, fn, i, key[i], true, emptyGet, raw ); - } + // jQuery.support is not used in Core but other projects attach their + // properties to it so it needs to exist. + support: support +} ); - // Sets one value - } else if ( value !== undefined ) { - chainable = true; - - if ( !jQuery.isFunction( value ) ) { - raw = true; - } - - if ( bulk ) { - // Bulk operations run against the entire set - if ( raw ) { - fn.call( elems, value ); - fn = null; - - // ...except when executing function values - } else { - bulk = fn; - fn = function( elem, key, value ) { - return bulk.call( jQuery( elem ), value ); - }; - } - } - - if ( fn ) { - for ( ; i < length; i++ ) { - fn( elems[i], key, raw ? value : value.call( elems[i], i, fn( elems[i], key ) ) ); - } - } - } - - return chainable ? - elems : - - // Gets - bulk ? - fn.call( elems ) : - length ? fn( elems[0], key ) : emptyGet; - }, - - now: function() { - return ( new Date() ).getTime(); - }, - - // A method for quickly swapping in/out CSS properties to get correct calculations. - // Note: this method belongs to the css module but it's needed here for the support module. - // If support gets modularized, this method should be moved back to the css module. - swap: function( elem, options, callback, args ) { - var ret, name, - old = {}; - - // Remember the old values, and insert the new ones - for ( name in options ) { - old[ name ] = elem.style[ name ]; - elem.style[ name ] = options[ name ]; - } - - ret = callback.apply( elem, args || [] ); - - // Revert the old values - for ( name in options ) { - elem.style[ name ] = old[ name ]; - } - - return ret; - } -}); - -jQuery.ready.promise = function( obj ) { - if ( !readyList ) { - - readyList = jQuery.Deferred(); - - // Catch cases where $(document).ready() is called after the browser event has already occurred. - // we once tried to use readyState "interactive" here, but it caused issues like the one - // discovered by ChrisS here: http://bugs.jquery.com/ticket/12282#comment:15 - if ( document.readyState === "complete" ) { - // Handle it asynchronously to allow scripts the opportunity to delay ready - setTimeout( jQuery.ready ); - - // Standards-based browsers support DOMContentLoaded - } else if ( document.addEventListener ) { - // Use the handy event callback - document.addEventListener( "DOMContentLoaded", completed, false ); - - // A fallback to window.onload, that will always work - window.addEventListener( "load", completed, false ); - - // If IE event model is used - } else { - // Ensure firing before onload, maybe late but safe also for iframes - document.attachEvent( "onreadystatechange", completed ); - - // A fallback to window.onload, that will always work - window.attachEvent( "onload", completed ); - - // If IE and not a frame - // continually check to see if the document is ready - var top = false; - - try { - top = window.frameElement == null && document.documentElement; - } catch(e) {} - - if ( top && top.doScroll ) { - (function doScrollCheck() { - if ( !jQuery.isReady ) { - - try { - // Use the trick by Diego Perini - // http://javascript.nwbox.com/IEContentLoaded/ - top.doScroll("left"); - } catch(e) { - return setTimeout( doScrollCheck, 50 ); - } - - // detach all dom ready events - detach(); - - // and execute any waiting functions - jQuery.ready(); - } - })(); - } - } - } - return readyList.promise( obj ); -}; +// JSHint would error on this code due to the Symbol not being defined in ES5. +// Defining this global in .jshintrc would create a danger of using the global +// unguarded in another place, it seems safer to just disable JSHint for these +// three lines. +/* jshint ignore: start */ +if ( typeof Symbol === "function" ) { + jQuery.fn[ Symbol.iterator ] = arr[ Symbol.iterator ]; +} +/* jshint ignore: end */ // Populate the class2type map -jQuery.each("Boolean Number String Function Array Date RegExp Object Error".split(" "), function(i, name) { +jQuery.each( "Boolean Number String Function Array Date RegExp Object Error Symbol".split( " " ), +function( i, name ) { class2type[ "[object " + name + "]" ] = name.toLowerCase(); -}); +} ); -function isArraylike( obj ) { - var length = obj.length, +function isArrayLike( obj ) { + + // Support: iOS 8.2 (not reproducible in simulator) + // `in` check used to prevent JIT error (gh-2145) + // hasOwn isn't used here due to false negatives + // regarding Nodelist length in IE + var length = !!obj && "length" in obj && obj.length, type = jQuery.type( obj ); - if ( jQuery.isWindow( obj ) ) { + if ( type === "function" || jQuery.isWindow( obj ) ) { return false; } - if ( obj.nodeType === 1 && length ) { - return true; - } - - return type === "array" || type !== "function" && - ( length === 0 || - typeof length === "number" && length > 0 && ( length - 1 ) in obj ); + return type === "array" || length === 0 || + typeof length === "number" && length > 0 && ( length - 1 ) in obj; } - -// All jQuery objects should point back to these -rootjQuery = jQuery(document); +var Sizzle = /*! - * Sizzle CSS Selector Engine v1.10.2 + * Sizzle CSS Selector Engine v2.2.1 * http://sizzlejs.com/ * - * Copyright 2013 jQuery Foundation, Inc. and other contributors + * Copyright jQuery Foundation and other contributors * Released under the MIT license * http://jquery.org/license * - * Date: 2013-07-03 + * Date: 2015-10-17 */ -(function( window, undefined ) { +(function( window ) { var i, support, - cachedruns, Expr, getText, isXML, + tokenize, compile, + select, outermostContext, sortInput, + hasDuplicate, // Local document vars setDocument, @@ -1033,24 +579,21 @@ var i, contains, // Instance-specific data - expando = "sizzle" + -(new Date()), + expando = "sizzle" + 1 * new Date(), preferredDoc = window.document, dirruns = 0, done = 0, classCache = createCache(), tokenCache = createCache(), compilerCache = createCache(), - hasDuplicate = false, sortOrder = function( a, b ) { if ( a === b ) { hasDuplicate = true; - return 0; } return 0; }, // General-purpose constants - strundefined = typeof undefined, MAX_NEGATIVE = 1 << 31, // Instance methods @@ -1060,12 +603,13 @@ var i, push_native = arr.push, push = arr.push, slice = arr.slice, - // Use a stripped-down indexOf if we can't use a native one - indexOf = arr.indexOf || function( elem ) { + // Use a stripped-down indexOf as it's faster than native + // http://jsperf.com/thor-indexof-vs-for/5 + indexOf = function( list, elem ) { var i = 0, - len = this.length; + len = list.length; for ( ; i < len; i++ ) { - if ( this[i] === elem ) { + if ( list[i] === elem ) { return i; } } @@ -1076,44 +620,46 @@ var i, // Regular expressions - // Whitespace characters http://www.w3.org/TR/css3-selectors/#whitespace + // http://www.w3.org/TR/css3-selectors/#whitespace whitespace = "[\\x20\\t\\r\\n\\f]", - // http://www.w3.org/TR/css3-syntax/#characters - characterEncoding = "(?:\\\\.|[\\w-]|[^\\x00-\\xa0])+", - // Loosely modeled on CSS identifier characters - // An unquoted value should be a CSS identifier http://www.w3.org/TR/css3-selectors/#attribute-selectors - // Proper syntax: http://www.w3.org/TR/CSS21/syndata.html#value-def-identifier - identifier = characterEncoding.replace( "w", "w#" ), + // http://www.w3.org/TR/CSS21/syndata.html#value-def-identifier + identifier = "(?:\\\\.|[\\w-]|[^\\x00-\\xa0])+", - // Acceptable operators http://www.w3.org/TR/selectors/#attribute-selectors - attributes = "\\[" + whitespace + "*(" + characterEncoding + ")" + whitespace + - "*(?:([*^$|!~]?=)" + whitespace + "*(?:(['\"])((?:\\\\.|[^\\\\])*?)\\3|(" + identifier + ")|)|)" + whitespace + "*\\]", + // Attribute selectors: http://www.w3.org/TR/selectors/#attribute-selectors + attributes = "\\[" + whitespace + "*(" + identifier + ")(?:" + whitespace + + // Operator (capture 2) + "*([*^$|!~]?=)" + whitespace + + // "Attribute values must be CSS identifiers [capture 5] or strings [capture 3 or capture 4]" + "*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|(" + identifier + "))|)" + whitespace + + "*\\]", - // Prefer arguments quoted, - // then not containing pseudos/brackets, - // then attribute selectors/non-parenthetical expressions, - // then anything else - // These preferences are here to reduce the number of selectors - // needing tokenize in the PSEUDO preFilter - pseudos = ":(" + characterEncoding + ")(?:\\(((['\"])((?:\\\\.|[^\\\\])*?)\\3|((?:\\\\.|[^\\\\()[\\]]|" + attributes.replace( 3, 8 ) + ")*)|.*)\\)|)", + pseudos = ":(" + identifier + ")(?:\\((" + + // To reduce the number of selectors needing tokenize in the preFilter, prefer arguments: + // 1. quoted (capture 3; capture 4 or capture 5) + "('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|" + + // 2. simple (capture 6) + "((?:\\\\.|[^\\\\()[\\]]|" + attributes + ")*)|" + + // 3. anything else (capture 2) + ".*" + + ")\\)|)", // Leading and non-escaped trailing whitespace, capturing some non-whitespace characters preceding the latter + rwhitespace = new RegExp( whitespace + "+", "g" ), rtrim = new RegExp( "^" + whitespace + "+|((?:^|[^\\\\])(?:\\\\.)*)" + whitespace + "+$", "g" ), rcomma = new RegExp( "^" + whitespace + "*," + whitespace + "*" ), rcombinators = new RegExp( "^" + whitespace + "*([>+~]|" + whitespace + ")" + whitespace + "*" ), - rsibling = new RegExp( whitespace + "*[+~]" ), - rattributeQuotes = new RegExp( "=" + whitespace + "*([^\\]'\"]*)" + whitespace + "*\\]", "g" ), + rattributeQuotes = new RegExp( "=" + whitespace + "*([^\\]'\"]*?)" + whitespace + "*\\]", "g" ), rpseudo = new RegExp( pseudos ), ridentifier = new RegExp( "^" + identifier + "$" ), matchExpr = { - "ID": new RegExp( "^#(" + characterEncoding + ")" ), - "CLASS": new RegExp( "^\\.(" + characterEncoding + ")" ), - "TAG": new RegExp( "^(" + characterEncoding.replace( "w", "w*" ) + ")" ), + "ID": new RegExp( "^#(" + identifier + ")" ), + "CLASS": new RegExp( "^\\.(" + identifier + ")" ), + "TAG": new RegExp( "^(" + identifier + "|[*])" ), "ATTR": new RegExp( "^" + attributes ), "PSEUDO": new RegExp( "^" + pseudos ), "CHILD": new RegExp( "^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\(" + whitespace + @@ -1126,14 +672,15 @@ var i, whitespace + "*((?:-\\d)?\\d*)" + whitespace + "*\\)|)(?=[^-]|$)", "i" ) }, + rinputs = /^(?:input|select|textarea|button)$/i, + rheader = /^h\d$/i, + rnative = /^[^{]+\{\s*\[native \w/, // Easily-parseable/retrievable ID or TAG or CLASS selectors rquickExpr = /^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/, - rinputs = /^(?:input|select|textarea|button)$/i, - rheader = /^h\d$/i, - + rsibling = /[+~]/, rescape = /'|\\/g, // CSS escapes http://www.w3.org/TR/CSS21/syndata.html#escaped-characters @@ -1141,15 +688,23 @@ var i, funescape = function( _, escaped, escapedWhitespace ) { var high = "0x" + escaped - 0x10000; // NaN means non-codepoint - // Support: Firefox + // Support: Firefox<24 // Workaround erroneous numeric interpretation of +"0x" return high !== high || escapedWhitespace ? escaped : - // BMP codepoint high < 0 ? + // BMP codepoint String.fromCharCode( high + 0x10000 ) : // Supplemental Plane codepoint (surrogate pair) String.fromCharCode( high >> 10 | 0xD800, high & 0x3FF | 0xDC00 ); + }, + + // Used for iframes + // See setDocument() + // Removing the function wrapper causes a "Permission Denied" + // error in IE + unloadHandler = function() { + setDocument(); }; // Optimize for push.apply( _, NodeList ) @@ -1182,104 +737,129 @@ try { } function Sizzle( selector, context, results, seed ) { - var match, elem, m, nodeType, - // QSA vars - i, groups, old, nid, newContext, newSelector; + var m, i, elem, nid, nidselect, match, groups, newSelector, + newContext = context && context.ownerDocument, - if ( ( context ? context.ownerDocument || context : preferredDoc ) !== document ) { - setDocument( context ); - } + // nodeType defaults to 9, since context defaults to document + nodeType = context ? context.nodeType : 9; - context = context || document; results = results || []; - if ( !selector || typeof selector !== "string" ) { + // Return early from calls with invalid selector or context + if ( typeof selector !== "string" || !selector || + nodeType !== 1 && nodeType !== 9 && nodeType !== 11 ) { + return results; } - if ( (nodeType = context.nodeType) !== 1 && nodeType !== 9 ) { - return []; - } + // Try to shortcut find operations (as opposed to filters) in HTML documents + if ( !seed ) { - if ( documentIsHTML && !seed ) { + if ( ( context ? context.ownerDocument || context : preferredDoc ) !== document ) { + setDocument( context ); + } + context = context || document; + + if ( documentIsHTML ) { + + // If the selector is sufficiently simple, try using a "get*By*" DOM method + // (excepting DocumentFragment context, where the methods don't exist) + if ( nodeType !== 11 && (match = rquickExpr.exec( selector )) ) { + + // ID selector + if ( (m = match[1]) ) { + + // Document context + if ( nodeType === 9 ) { + if ( (elem = context.getElementById( m )) ) { + + // Support: IE, Opera, Webkit + // TODO: identify versions + // getElementById can match elements by name instead of ID + if ( elem.id === m ) { + results.push( elem ); + return results; + } + } else { + return results; + } + + // Element context + } else { + + // Support: IE, Opera, Webkit + // TODO: identify versions + // getElementById can match elements by name instead of ID + if ( newContext && (elem = newContext.getElementById( m )) && + contains( context, elem ) && + elem.id === m ) { - // Shortcuts - if ( (match = rquickExpr.exec( selector )) ) { - // Speed-up: Sizzle("#ID") - if ( (m = match[1]) ) { - if ( nodeType === 9 ) { - elem = context.getElementById( m ); - // Check parentNode to catch when Blackberry 4.6 returns - // nodes that are no longer in the document #6963 - if ( elem && elem.parentNode ) { - // Handle the case where IE, Opera, and Webkit return items - // by name instead of ID - if ( elem.id === m ) { results.push( elem ); return results; } - } else { - return results; } - } else { - // Context is not a document - if ( context.ownerDocument && (elem = context.ownerDocument.getElementById( m )) && - contains( context, elem ) && elem.id === m ) { - results.push( elem ); - return results; - } - } - // Speed-up: Sizzle("TAG") - } else if ( match[2] ) { - push.apply( results, context.getElementsByTagName( selector ) ); - return results; - - // Speed-up: Sizzle(".CLASS") - } else if ( (m = match[3]) && support.getElementsByClassName && context.getElementsByClassName ) { - push.apply( results, context.getElementsByClassName( m ) ); - return results; - } - } - - // QSA path - if ( support.qsa && (!rbuggyQSA || !rbuggyQSA.test( selector )) ) { - nid = old = expando; - newContext = context; - newSelector = nodeType === 9 && selector; - - // qSA works strangely on Element-rooted queries - // We can work around this by specifying an extra ID on the root - // and working up from there (Thanks to Andrew Dupont for the technique) - // IE 8 doesn't work on object elements - if ( nodeType === 1 && context.nodeName.toLowerCase() !== "object" ) { - groups = tokenize( selector ); - - if ( (old = context.getAttribute("id")) ) { - nid = old.replace( rescape, "\\$&" ); - } else { - context.setAttribute( "id", nid ); - } - nid = "[id='" + nid + "'] "; - - i = groups.length; - while ( i-- ) { - groups[i] = nid + toSelector( groups[i] ); - } - newContext = rsibling.test( selector ) && context.parentNode || context; - newSelector = groups.join(","); - } - - if ( newSelector ) { - try { - push.apply( results, - newContext.querySelectorAll( newSelector ) - ); + // Type selector + } else if ( match[2] ) { + push.apply( results, context.getElementsByTagName( selector ) ); return results; - } catch(qsaError) { - } finally { - if ( !old ) { - context.removeAttribute("id"); + + // Class selector + } else if ( (m = match[3]) && support.getElementsByClassName && + context.getElementsByClassName ) { + + push.apply( results, context.getElementsByClassName( m ) ); + return results; + } + } + + // Take advantage of querySelectorAll + if ( support.qsa && + !compilerCache[ selector + " " ] && + (!rbuggyQSA || !rbuggyQSA.test( selector )) ) { + + if ( nodeType !== 1 ) { + newContext = context; + newSelector = selector; + + // qSA looks outside Element context, which is not what we want + // Thanks to Andrew Dupont for this workaround technique + // Support: IE <=8 + // Exclude object elements + } else if ( context.nodeName.toLowerCase() !== "object" ) { + + // Capture the context ID, setting it first if necessary + if ( (nid = context.getAttribute( "id" )) ) { + nid = nid.replace( rescape, "\\$&" ); + } else { + context.setAttribute( "id", (nid = expando) ); + } + + // Prefix every selector in the list + groups = tokenize( selector ); + i = groups.length; + nidselect = ridentifier.test( nid ) ? "#" + nid : "[id='" + nid + "']"; + while ( i-- ) { + groups[i] = nidselect + " " + toSelector( groups[i] ); + } + newSelector = groups.join( "," ); + + // Expand context for sibling selectors + newContext = rsibling.test( selector ) && testContext( context.parentNode ) || + context; + } + + if ( newSelector ) { + try { + push.apply( results, + newContext.querySelectorAll( newSelector ) + ); + return results; + } catch ( qsaError ) { + } finally { + if ( nid === expando ) { + context.removeAttribute( "id" ); + } } } } @@ -1292,7 +872,7 @@ function Sizzle( selector, context, results, seed ) { /** * Create key-value caches of limited size - * @returns {Function(string, Object)} Returns the Object data after storing it on itself with + * @returns {function(string, object)} Returns the Object data after storing it on itself with * property name the (space-suffixed) string and (if the cache is larger than Expr.cacheLength) * deleting the oldest entry */ @@ -1301,11 +881,11 @@ function createCache() { function cache( key, value ) { // Use (key + " ") to avoid collision with native prototype properties (see Issue #157) - if ( keys.push( key += " " ) > Expr.cacheLength ) { + if ( keys.push( key + " " ) > Expr.cacheLength ) { // Only keep the most recent entries delete cache[ keys.shift() ]; } - return (cache[ key ] = value); + return (cache[ key + " " ] = value); } return cache; } @@ -1347,7 +927,7 @@ function assert( fn ) { */ function addHandle( attrs, handler ) { var arr = attrs.split("|"), - i = attrs.length; + i = arr.length; while ( i-- ) { Expr.attrHandle[ arr[i] ] = handler; @@ -1428,8 +1008,21 @@ function createPositionalPseudo( fn ) { } /** - * Detect xml + * Checks a node for validity as a Sizzle context + * @param {Element|Object=} context + * @returns {Element|Object|Boolean} The input node if acceptable, otherwise a falsy value + */ +function testContext( context ) { + return context && typeof context.getElementsByTagName !== "undefined" && context; +} + +// Expose support vars for convenience +support = Sizzle.support = {}; + +/** + * Detects XML nodes * @param {Element|Object} elem An element or a document + * @returns {Boolean} True iff elem is a non-HTML XML node */ isXML = Sizzle.isXML = function( elem ) { // documentElement is verified for cases where it doesn't yet exist @@ -1438,45 +1031,44 @@ isXML = Sizzle.isXML = function( elem ) { return documentElement ? documentElement.nodeName !== "HTML" : false; }; -// Expose support vars for convenience -support = Sizzle.support = {}; - /** * Sets document-related variables once based on the current document * @param {Element|Object} [doc] An element or document object to use to set the document * @returns {Object} Returns the current document */ setDocument = Sizzle.setDocument = function( node ) { - var doc = node ? node.ownerDocument || node : preferredDoc, - parent = doc.defaultView; + var hasCompare, parent, + doc = node ? node.ownerDocument || node : preferredDoc; - // If no document and documentElement is available, return + // Return early if doc is invalid or already selected if ( doc === document || doc.nodeType !== 9 || !doc.documentElement ) { return document; } - // Set our document + // Update global variables document = doc; - docElem = doc.documentElement; + docElem = document.documentElement; + documentIsHTML = !isXML( document ); - // Support tests - documentIsHTML = !isXML( doc ); + // Support: IE 9-11, Edge + // Accessing iframe documents after unload throws "permission denied" errors (jQuery #13936) + if ( (parent = document.defaultView) && parent.top !== parent ) { + // Support: IE 11 + if ( parent.addEventListener ) { + parent.addEventListener( "unload", unloadHandler, false ); - // Support: IE>8 - // If iframe document is assigned to "document" variable and if iframe has been reloaded, - // IE will throw "permission denied" error when accessing "document" variable, see jQuery #13936 - // IE6-8 do not support the defaultView property so parent will be undefined - if ( parent && parent.attachEvent && parent !== parent.top ) { - parent.attachEvent( "onbeforeunload", function() { - setDocument(); - }); + // Support: IE 9 - 10 only + } else if ( parent.attachEvent ) { + parent.attachEvent( "onunload", unloadHandler ); + } } /* Attributes ---------------------------------------------------------------------- */ // Support: IE<8 - // Verify that getAttribute really returns attributes and not properties (excepting IE8 booleans) + // Verify that getAttribute really returns attributes and not properties + // (excepting IE8 booleans) support.attributes = assert(function( div ) { div.className = "i"; return !div.getAttribute("className"); @@ -1487,21 +1079,12 @@ setDocument = Sizzle.setDocument = function( node ) { // Check if getElementsByTagName("*") returns only elements support.getElementsByTagName = assert(function( div ) { - div.appendChild( doc.createComment("") ); + div.appendChild( document.createComment("") ); return !div.getElementsByTagName("*").length; }); - // Check if getElementsByClassName can be trusted - support.getElementsByClassName = assert(function( div ) { - div.innerHTML = "
        "; - - // Support: Safari<4 - // Catch class over-caching - div.firstChild.className = "i"; - // Support: Opera<10 - // Catch gEBCN failure to find non-leading classes - return div.getElementsByClassName("i").length === 2; - }); + // Support: IE<9 + support.getElementsByClassName = rnative.test( document.getElementsByClassName ); // Support: IE<10 // Check if getElementById returns elements by name @@ -1509,17 +1092,15 @@ setDocument = Sizzle.setDocument = function( node ) { // so use a roundabout getElementsByName test support.getById = assert(function( div ) { docElem.appendChild( div ).id = expando; - return !doc.getElementsByName || !doc.getElementsByName( expando ).length; + return !document.getElementsByName || !document.getElementsByName( expando ).length; }); // ID find and filter if ( support.getById ) { Expr.find["ID"] = function( id, context ) { - if ( typeof context.getElementById !== strundefined && documentIsHTML ) { + if ( typeof context.getElementById !== "undefined" && documentIsHTML ) { var m = context.getElementById( id ); - // Check parentNode to catch when Blackberry 4.6 returns - // nodes that are no longer in the document #6963 - return m && m.parentNode ? [m] : []; + return m ? [ m ] : []; } }; Expr.filter["ID"] = function( id ) { @@ -1536,7 +1117,8 @@ setDocument = Sizzle.setDocument = function( node ) { Expr.filter["ID"] = function( id ) { var attrId = id.replace( runescape, funescape ); return function( elem ) { - var node = typeof elem.getAttributeNode !== strundefined && elem.getAttributeNode("id"); + var node = typeof elem.getAttributeNode !== "undefined" && + elem.getAttributeNode("id"); return node && node.value === attrId; }; }; @@ -1545,14 +1127,20 @@ setDocument = Sizzle.setDocument = function( node ) { // Tag Expr.find["TAG"] = support.getElementsByTagName ? function( tag, context ) { - if ( typeof context.getElementsByTagName !== strundefined ) { + if ( typeof context.getElementsByTagName !== "undefined" ) { return context.getElementsByTagName( tag ); + + // DocumentFragment nodes don't have gEBTN + } else if ( support.qsa ) { + return context.querySelectorAll( tag ); } } : + function( tag, context ) { var elem, tmp = [], i = 0, + // By happy coincidence, a (broken) gEBTN appears on DocumentFragment nodes too results = context.getElementsByTagName( tag ); // Filter out possible comments @@ -1570,7 +1158,7 @@ setDocument = Sizzle.setDocument = function( node ) { // Class Expr.find["CLASS"] = support.getElementsByClassName && function( className, context ) { - if ( typeof context.getElementsByClassName !== strundefined && documentIsHTML ) { + if ( typeof context.getElementsByClassName !== "undefined" && documentIsHTML ) { return context.getElementsByClassName( className ); } }; @@ -1590,7 +1178,7 @@ setDocument = Sizzle.setDocument = function( node ) { // See http://bugs.jquery.com/ticket/13378 rbuggyQSA = []; - if ( (support.qsa = rnative.test( doc.querySelectorAll )) ) { + if ( (support.qsa = rnative.test( document.querySelectorAll )) ) { // Build QSA regex // Regex strategy adopted from Diego Perini assert(function( div ) { @@ -1599,7 +1187,17 @@ setDocument = Sizzle.setDocument = function( node ) { // setting a boolean content attribute, // since its presence should be enough // http://bugs.jquery.com/ticket/12359 - div.innerHTML = ""; + docElem.appendChild( div ).innerHTML = "" + + ""; + + // Support: IE8, Opera 11-12.16 + // Nothing should be selected when empty strings follow ^= or $= or *= + // The test attribute must be unknown in Opera but "safe" for WinRT + // http://msdn.microsoft.com/en-us/library/ie/hh465388.aspx#attribute_section + if ( div.querySelectorAll("[msallowcapture^='']").length ) { + rbuggyQSA.push( "[*^$]=" + whitespace + "*(?:''|\"\")" ); + } // Support: IE8 // Boolean attributes and "value" are not treated correctly @@ -1607,27 +1205,37 @@ setDocument = Sizzle.setDocument = function( node ) { rbuggyQSA.push( "\\[" + whitespace + "*(?:value|" + booleans + ")" ); } + // Support: Chrome<29, Android<4.4, Safari<7.0+, iOS<7.0+, PhantomJS<1.9.8+ + if ( !div.querySelectorAll( "[id~=" + expando + "-]" ).length ) { + rbuggyQSA.push("~="); + } + // Webkit/Opera - :checked should return selected option elements // http://www.w3.org/TR/2011/REC-css3-selectors-20110929/#checked // IE8 throws error here and will not see later tests if ( !div.querySelectorAll(":checked").length ) { rbuggyQSA.push(":checked"); } + + // Support: Safari 8+, iOS 8+ + // https://bugs.webkit.org/show_bug.cgi?id=136851 + // In-page `selector#id sibing-combinator selector` fails + if ( !div.querySelectorAll( "a#" + expando + "+*" ).length ) { + rbuggyQSA.push(".#.+[+~]"); + } }); assert(function( div ) { - - // Support: Opera 10-12/IE8 - // ^= $= *= and empty values - // Should not select anything // Support: Windows 8 Native Apps - // The type attribute is restricted during .innerHTML assignment - var input = doc.createElement("input"); + // The type and name attributes are restricted during .innerHTML assignment + var input = document.createElement("input"); input.setAttribute( "type", "hidden" ); - div.appendChild( input ).setAttribute( "t", "" ); + div.appendChild( input ).setAttribute( "name", "D" ); - if ( div.querySelectorAll("[t^='']").length ) { - rbuggyQSA.push( "[*^$]=" + whitespace + "*(?:''|\"\")" ); + // Support: IE8 + // Enforce case-sensitivity of name attribute + if ( div.querySelectorAll("[name=d]").length ) { + rbuggyQSA.push( "name" + whitespace + "*[*^$|!~]?=" ); } // FF 3.5 - :enabled/:disabled and hidden elements (hidden elements are still enabled) @@ -1642,7 +1250,8 @@ setDocument = Sizzle.setDocument = function( node ) { }); } - if ( (support.matchesSelector = rnative.test( (matches = docElem.webkitMatchesSelector || + if ( (support.matchesSelector = rnative.test( (matches = docElem.matches || + docElem.webkitMatchesSelector || docElem.mozMatchesSelector || docElem.oMatchesSelector || docElem.msMatchesSelector) )) ) { @@ -1664,11 +1273,12 @@ setDocument = Sizzle.setDocument = function( node ) { /* Contains ---------------------------------------------------------------------- */ + hasCompare = rnative.test( docElem.compareDocumentPosition ); // Element contains another - // Purposefully does not implement inclusive descendent + // Purposefully self-exclusive // As in, an element does not contain itself - contains = rnative.test( docElem.contains ) || docElem.compareDocumentPosition ? + contains = hasCompare || rnative.test( docElem.contains ) ? function( a, b ) { var adown = a.nodeType === 9 ? a.documentElement : a, bup = b && b.parentNode; @@ -1693,7 +1303,7 @@ setDocument = Sizzle.setDocument = function( node ) { ---------------------------------------------------------------------- */ // Document order sorting - sortOrder = docElem.compareDocumentPosition ? + sortOrder = hasCompare ? function( a, b ) { // Flag for duplicate removal @@ -1702,34 +1312,46 @@ setDocument = Sizzle.setDocument = function( node ) { return 0; } - var compare = b.compareDocumentPosition && a.compareDocumentPosition && a.compareDocumentPosition( b ); - + // Sort on method existence if only one input has compareDocumentPosition + var compare = !a.compareDocumentPosition - !b.compareDocumentPosition; if ( compare ) { - // Disconnected nodes - if ( compare & 1 || - (!support.sortDetached && b.compareDocumentPosition( a ) === compare) ) { - - // Choose the first element that is related to our preferred document - if ( a === doc || contains(preferredDoc, a) ) { - return -1; - } - if ( b === doc || contains(preferredDoc, b) ) { - return 1; - } - - // Maintain original order - return sortInput ? - ( indexOf.call( sortInput, a ) - indexOf.call( sortInput, b ) ) : - 0; - } - - return compare & 4 ? -1 : 1; + return compare; } - // Not directly comparable, sort on existence of method - return a.compareDocumentPosition ? -1 : 1; + // Calculate position if both inputs belong to the same document + compare = ( a.ownerDocument || a ) === ( b.ownerDocument || b ) ? + a.compareDocumentPosition( b ) : + + // Otherwise we know they are disconnected + 1; + + // Disconnected nodes + if ( compare & 1 || + (!support.sortDetached && b.compareDocumentPosition( a ) === compare) ) { + + // Choose the first element that is related to our preferred document + if ( a === document || a.ownerDocument === preferredDoc && contains(preferredDoc, a) ) { + return -1; + } + if ( b === document || b.ownerDocument === preferredDoc && contains(preferredDoc, b) ) { + return 1; + } + + // Maintain original order + return sortInput ? + ( indexOf( sortInput, a ) - indexOf( sortInput, b ) ) : + 0; + } + + return compare & 4 ? -1 : 1; } : function( a, b ) { + // Exit early if the nodes are identical + if ( a === b ) { + hasDuplicate = true; + return 0; + } + var cur, i = 0, aup = a.parentNode, @@ -1737,19 +1359,14 @@ setDocument = Sizzle.setDocument = function( node ) { ap = [ a ], bp = [ b ]; - // Exit early if the nodes are identical - if ( a === b ) { - hasDuplicate = true; - return 0; - // Parentless nodes are either documents or disconnected - } else if ( !aup || !bup ) { - return a === doc ? -1 : - b === doc ? 1 : + if ( !aup || !bup ) { + return a === document ? -1 : + b === document ? 1 : aup ? -1 : bup ? 1 : sortInput ? - ( indexOf.call( sortInput, a ) - indexOf.call( sortInput, b ) ) : + ( indexOf( sortInput, a ) - indexOf( sortInput, b ) ) : 0; // If the nodes are siblings, we can do a quick check @@ -1782,7 +1399,7 @@ setDocument = Sizzle.setDocument = function( node ) { 0; }; - return doc; + return document; }; Sizzle.matches = function( expr, elements ) { @@ -1799,6 +1416,7 @@ Sizzle.matchesSelector = function( elem, expr ) { expr = expr.replace( rattributeQuotes, "='$1']" ); if ( support.matchesSelector && documentIsHTML && + !compilerCache[ expr + " " ] && ( !rbuggyMatches || !rbuggyMatches.test( expr ) ) && ( !rbuggyQSA || !rbuggyQSA.test( expr ) ) ) { @@ -1812,10 +1430,10 @@ Sizzle.matchesSelector = function( elem, expr ) { elem.document && elem.document.nodeType !== 11 ) { return ret; } - } catch(e) {} + } catch (e) {} } - return Sizzle( expr, document, null, [elem] ).length > 0; + return Sizzle( expr, document, null, [ elem ] ).length > 0; }; Sizzle.contains = function( context, elem ) { @@ -1838,13 +1456,13 @@ Sizzle.attr = function( elem, name ) { fn( elem, name, !documentIsHTML ) : undefined; - return val === undefined ? + return val !== undefined ? + val : support.attributes || !documentIsHTML ? elem.getAttribute( name ) : (val = elem.getAttributeNode(name)) && val.specified ? val.value : - null : - val; + null; }; Sizzle.error = function( msg ) { @@ -1877,6 +1495,10 @@ Sizzle.uniqueSort = function( results ) { } } + // Clear input after sorting to release objects + // See https://github.com/jquery/sizzle/pull/225 + sortInput = null; + return results; }; @@ -1892,13 +1514,13 @@ getText = Sizzle.getText = function( elem ) { if ( !nodeType ) { // If no nodeType, this is expected to be an array - for ( ; (node = elem[i]); i++ ) { + while ( (node = elem[i++]) ) { // Do not traverse comment nodes ret += getText( node ); } } else if ( nodeType === 1 || nodeType === 9 || nodeType === 11 ) { // Use textContent for elements - // innerText usage removed for consistency of new lines (see #11153) + // innerText usage removed for consistency of new lines (jQuery #11153) if ( typeof elem.textContent === "string" ) { return elem.textContent; } else { @@ -1940,7 +1562,7 @@ Expr = Sizzle.selectors = { match[1] = match[1].replace( runescape, funescape ); // Move the given value to match[3] whether quoted or unquoted - match[3] = ( match[4] || match[5] || "" ).replace( runescape, funescape ); + match[3] = ( match[3] || match[4] || match[5] || "" ).replace( runescape, funescape ); if ( match[2] === "~=" ) { match[3] = " " + match[3] + " "; @@ -1983,15 +1605,15 @@ Expr = Sizzle.selectors = { "PSEUDO": function( match ) { var excess, - unquoted = !match[5] && match[2]; + unquoted = !match[6] && match[2]; if ( matchExpr["CHILD"].test( match[0] ) ) { return null; } // Accept quoted arguments as-is - if ( match[3] && match[4] !== undefined ) { - match[2] = match[4]; + if ( match[3] ) { + match[2] = match[4] || match[5] || ""; // Strip excess characters from unquoted arguments } else if ( unquoted && rpseudo.test( unquoted ) && @@ -2027,7 +1649,7 @@ Expr = Sizzle.selectors = { return pattern || (pattern = new RegExp( "(^|" + whitespace + ")" + className + "(" + whitespace + "|$)" )) && classCache( className, function( elem ) { - return pattern.test( typeof elem.className === "string" && elem.className || typeof elem.getAttribute !== strundefined && elem.getAttribute("class") || "" ); + return pattern.test( typeof elem.className === "string" && elem.className || typeof elem.getAttribute !== "undefined" && elem.getAttribute("class") || "" ); }); }, @@ -2049,7 +1671,7 @@ Expr = Sizzle.selectors = { operator === "^=" ? check && result.indexOf( check ) === 0 : operator === "*=" ? check && result.indexOf( check ) > -1 : operator === "$=" ? check && result.slice( -check.length ) === check : - operator === "~=" ? ( " " + result + " " ).indexOf( check ) > -1 : + operator === "~=" ? ( " " + result.replace( rwhitespace, " " ) + " " ).indexOf( check ) > -1 : operator === "|=" ? result === check || result.slice( 0, check.length + 1 ) === check + "-" : false; }; @@ -2068,11 +1690,12 @@ Expr = Sizzle.selectors = { } : function( elem, context, xml ) { - var cache, outerCache, node, diff, nodeIndex, start, + var cache, uniqueCache, outerCache, node, nodeIndex, start, dir = simple !== forward ? "nextSibling" : "previousSibling", parent = elem.parentNode, name = ofType && elem.nodeName.toLowerCase(), - useCache = !xml && !ofType; + useCache = !xml && !ofType, + diff = false; if ( parent ) { @@ -2081,7 +1704,10 @@ Expr = Sizzle.selectors = { while ( dir ) { node = elem; while ( (node = node[ dir ]) ) { - if ( ofType ? node.nodeName.toLowerCase() === name : node.nodeType === 1 ) { + if ( ofType ? + node.nodeName.toLowerCase() === name : + node.nodeType === 1 ) { + return false; } } @@ -2095,11 +1721,21 @@ Expr = Sizzle.selectors = { // non-xml :nth-child(...) stores cache data on `parent` if ( forward && useCache ) { + // Seek `elem` from a previously-cached index - outerCache = parent[ expando ] || (parent[ expando ] = {}); - cache = outerCache[ type ] || []; - nodeIndex = cache[0] === dirruns && cache[1]; - diff = cache[0] === dirruns && cache[2]; + + // ...in a gzip-friendly way + node = parent; + outerCache = node[ expando ] || (node[ expando ] = {}); + + // Support: IE <9 only + // Defend against cloned attroperties (jQuery gh-1709) + uniqueCache = outerCache[ node.uniqueID ] || + (outerCache[ node.uniqueID ] = {}); + + cache = uniqueCache[ type ] || []; + nodeIndex = cache[ 0 ] === dirruns && cache[ 1 ]; + diff = nodeIndex && cache[ 2 ]; node = nodeIndex && parent.childNodes[ nodeIndex ]; while ( (node = ++nodeIndex && node && node[ dir ] || @@ -2109,29 +1745,55 @@ Expr = Sizzle.selectors = { // When found, cache indexes on `parent` and break if ( node.nodeType === 1 && ++diff && node === elem ) { - outerCache[ type ] = [ dirruns, nodeIndex, diff ]; + uniqueCache[ type ] = [ dirruns, nodeIndex, diff ]; break; } } - // Use previously-cached element index if available - } else if ( useCache && (cache = (elem[ expando ] || (elem[ expando ] = {}))[ type ]) && cache[0] === dirruns ) { - diff = cache[1]; - - // xml :nth-child(...) or :nth-last-child(...) or :nth(-last)?-of-type(...) } else { - // Use the same loop as above to seek `elem` from the start - while ( (node = ++nodeIndex && node && node[ dir ] || - (diff = nodeIndex = 0) || start.pop()) ) { + // Use previously-cached element index if available + if ( useCache ) { + // ...in a gzip-friendly way + node = elem; + outerCache = node[ expando ] || (node[ expando ] = {}); - if ( ( ofType ? node.nodeName.toLowerCase() === name : node.nodeType === 1 ) && ++diff ) { - // Cache the index of each encountered element - if ( useCache ) { - (node[ expando ] || (node[ expando ] = {}))[ type ] = [ dirruns, diff ]; - } + // Support: IE <9 only + // Defend against cloned attroperties (jQuery gh-1709) + uniqueCache = outerCache[ node.uniqueID ] || + (outerCache[ node.uniqueID ] = {}); - if ( node === elem ) { - break; + cache = uniqueCache[ type ] || []; + nodeIndex = cache[ 0 ] === dirruns && cache[ 1 ]; + diff = nodeIndex; + } + + // xml :nth-child(...) + // or :nth-last-child(...) or :nth(-last)?-of-type(...) + if ( diff === false ) { + // Use the same loop as above to seek `elem` from the start + while ( (node = ++nodeIndex && node && node[ dir ] || + (diff = nodeIndex = 0) || start.pop()) ) { + + if ( ( ofType ? + node.nodeName.toLowerCase() === name : + node.nodeType === 1 ) && + ++diff ) { + + // Cache the index of each encountered element + if ( useCache ) { + outerCache = node[ expando ] || (node[ expando ] = {}); + + // Support: IE <9 only + // Defend against cloned attroperties (jQuery gh-1709) + uniqueCache = outerCache[ node.uniqueID ] || + (outerCache[ node.uniqueID ] = {}); + + uniqueCache[ type ] = [ dirruns, diff ]; + } + + if ( node === elem ) { + break; + } } } } @@ -2169,7 +1831,7 @@ Expr = Sizzle.selectors = { matched = fn( seed, argument ), i = matched.length; while ( i-- ) { - idx = indexOf.call( seed, matched[i] ); + idx = indexOf( seed, matched[i] ); seed[ idx ] = !( matches[ idx ] = matched[i] ); } }) : @@ -2208,6 +1870,8 @@ Expr = Sizzle.selectors = { function( elem, context, xml ) { input[0] = elem; matcher( input, null, xml, results ); + // Don't keep the element (issue #299) + input[0] = null; return !results.pop(); }; }), @@ -2219,6 +1883,7 @@ Expr = Sizzle.selectors = { }), "contains": markFunction(function( text ) { + text = text.replace( runescape, funescape ); return function( elem ) { return ( elem.textContent || elem.innerText || getText( elem ) ).indexOf( text ) > -1; }; @@ -2295,12 +1960,11 @@ Expr = Sizzle.selectors = { // Contents "empty": function( elem ) { // http://www.w3.org/TR/selectors/#empty-pseudo - // :empty is only affected by element nodes and content nodes(including text(3), cdata(4)), - // not comment, processing instructions, or others - // Thanks to Diego Perini for the nodeName shortcut - // Greater than "@" means alpha characters (specifically not starting with "#" or "?") + // :empty is negated by element (1) or content nodes (text: 3; cdata: 4; entity ref: 5), + // but not by others (comment: 8; processing instruction: 7; etc.) + // nodeType < 6 works because attributes (2) do not appear as children for ( elem = elem.firstChild; elem; elem = elem.nextSibling ) { - if ( elem.nodeName > "@" || elem.nodeType === 3 || elem.nodeType === 4 ) { + if ( elem.nodeType < 6 ) { return false; } } @@ -2327,11 +1991,12 @@ Expr = Sizzle.selectors = { "text": function( elem ) { var attr; - // IE6 and 7 will map elem.type to 'text' for new HTML5 types (search, etc) - // use getAttribute instead to test this case return elem.nodeName.toLowerCase() === "input" && elem.type === "text" && - ( (attr = elem.getAttribute("type")) == null || attr.toLowerCase() === elem.type ); + + // Support: IE<8 + // New HTML5 attribute values (e.g., "search") appear with elem.type === "text" + ( (attr = elem.getAttribute("type")) == null || attr.toLowerCase() === "text" ); }, // Position-in-collection @@ -2396,7 +2061,7 @@ function setFilters() {} setFilters.prototype = Expr.filters = Expr.pseudos; Expr.setFilters = new setFilters(); -function tokenize( selector, parseOnly ) { +tokenize = Sizzle.tokenize = function( selector, parseOnly ) { var matched, match, tokens, type, soFar, groups, preFilters, cached = tokenCache[ selector + " " ]; @@ -2417,7 +2082,7 @@ function tokenize( selector, parseOnly ) { // Don't consume trailing commas as valid soFar = soFar.slice( match[0].length ) || soFar; } - groups.push( tokens = [] ); + groups.push( (tokens = []) ); } matched = false; @@ -2461,7 +2126,7 @@ function tokenize( selector, parseOnly ) { Sizzle.error( selector ) : // Cache the tokens tokenCache( selector, groups ).slice( 0 ); -} +}; function toSelector( tokens ) { var i = 0, @@ -2490,10 +2155,10 @@ function addCombinator( matcher, combinator, base ) { // Check against all ancestor/preceding elements function( elem, context, xml ) { - var data, cache, outerCache, - dirkey = dirruns + " " + doneName; + var oldCache, uniqueCache, outerCache, + newCache = [ dirruns, doneName ]; - // We can't set arbitrary data on XML nodes, so they don't benefit from dir caching + // We can't set arbitrary data on XML nodes, so they don't benefit from combinator caching if ( xml ) { while ( (elem = elem[ dir ]) ) { if ( elem.nodeType === 1 || checkNonElements ) { @@ -2506,14 +2171,22 @@ function addCombinator( matcher, combinator, base ) { while ( (elem = elem[ dir ]) ) { if ( elem.nodeType === 1 || checkNonElements ) { outerCache = elem[ expando ] || (elem[ expando ] = {}); - if ( (cache = outerCache[ dir ]) && cache[0] === dirkey ) { - if ( (data = cache[1]) === true || data === cachedruns ) { - return data === true; - } + + // Support: IE <9 only + // Defend against cloned attroperties (jQuery gh-1709) + uniqueCache = outerCache[ elem.uniqueID ] || (outerCache[ elem.uniqueID ] = {}); + + if ( (oldCache = uniqueCache[ dir ]) && + oldCache[ 0 ] === dirruns && oldCache[ 1 ] === doneName ) { + + // Assign to newCache so results back-propagate to previous elements + return (newCache[ 2 ] = oldCache[ 2 ]); } else { - cache = outerCache[ dir ] = [ dirkey ]; - cache[1] = matcher( elem, context, xml ) || cachedruns; - if ( cache[1] === true ) { + // Reuse newcache so results back-propagate to previous elements + uniqueCache[ dir ] = newCache; + + // A match means we're done; a fail means we have to keep checking + if ( (newCache[ 2 ] = matcher( elem, context, xml )) ) { return true; } } @@ -2537,6 +2210,15 @@ function elementMatcher( matchers ) { matchers[0]; } +function multipleContexts( selector, contexts, results ) { + var i = 0, + len = contexts.length; + for ( ; i < len; i++ ) { + Sizzle( selector, contexts[i], results ); + } + return results; +} + function condense( unmatched, map, filter, context, xml ) { var elem, newUnmatched = [], @@ -2628,7 +2310,7 @@ function setMatcher( preFilter, selector, matcher, postFilter, postFinder, postS i = matcherOut.length; while ( i-- ) { if ( (elem = matcherOut[i]) && - (temp = postFinder ? indexOf.call( seed, elem ) : preMap[i]) > -1 ) { + (temp = postFinder ? indexOf( seed, elem ) : preMap[i]) > -1 ) { seed[temp] = !(results[temp] = elem); } @@ -2663,13 +2345,16 @@ function matcherFromTokens( tokens ) { return elem === checkContext; }, implicitRelative, true ), matchAnyContext = addCombinator( function( elem ) { - return indexOf.call( checkContext, elem ) > -1; + return indexOf( checkContext, elem ) > -1; }, implicitRelative, true ), matchers = [ function( elem, context, xml ) { - return ( !leadingRelative && ( xml || context !== outermostContext ) ) || ( + var ret = ( !leadingRelative && ( xml || context !== outermostContext ) ) || ( (checkContext = context).nodeType ? matchContext( elem, context, xml ) : matchAnyContext( elem, context, xml ) ); + // Avoid hanging onto element (issue #299) + checkContext = null; + return ret; } ]; for ( ; i < len; i++ ) { @@ -2707,42 +2392,43 @@ function matcherFromTokens( tokens ) { } function matcherFromGroupMatchers( elementMatchers, setMatchers ) { - // A counter to specify which element is currently being matched - var matcherCachedRuns = 0, - bySet = setMatchers.length > 0, + var bySet = setMatchers.length > 0, byElement = elementMatchers.length > 0, - superMatcher = function( seed, context, xml, results, expandContext ) { + superMatcher = function( seed, context, xml, results, outermost ) { var elem, j, matcher, - setMatched = [], matchedCount = 0, i = "0", unmatched = seed && [], - outermost = expandContext != null, + setMatched = [], contextBackup = outermostContext, - // We must always have either seed elements or context - elems = seed || byElement && Expr.find["TAG"]( "*", expandContext && context.parentNode || context ), + // We must always have either seed elements or outermost context + elems = seed || byElement && Expr.find["TAG"]( "*", outermost ), // Use integer dirruns iff this is the outermost matcher - dirrunsUnique = (dirruns += contextBackup == null ? 1 : Math.random() || 0.1); + dirrunsUnique = (dirruns += contextBackup == null ? 1 : Math.random() || 0.1), + len = elems.length; if ( outermost ) { - outermostContext = context !== document && context; - cachedruns = matcherCachedRuns; + outermostContext = context === document || context || outermost; } // Add elements passing elementMatchers directly to results - // Keep `i` a string if there are no elements so `matchedCount` will be "00" below - for ( ; (elem = elems[i]) != null; i++ ) { + // Support: IE<9, Safari + // Tolerate NodeList properties (IE: "length"; Safari: ) matching elements by id + for ( ; i !== len && (elem = elems[i]) != null; i++ ) { if ( byElement && elem ) { j = 0; + if ( !context && elem.ownerDocument !== document ) { + setDocument( elem ); + xml = !documentIsHTML; + } while ( (matcher = elementMatchers[j++]) ) { - if ( matcher( elem, context, xml ) ) { + if ( matcher( elem, context || document, xml) ) { results.push( elem ); break; } } if ( outermost ) { dirruns = dirrunsUnique; - cachedruns = ++matcherCachedRuns; } } @@ -2760,8 +2446,17 @@ function matcherFromGroupMatchers( elementMatchers, setMatchers ) { } } - // Apply set filters to unmatched elements + // `i` is now the count of elements visited above, and adding it to `matchedCount` + // makes the latter nonnegative. matchedCount += i; + + // Apply set filters to unmatched elements + // NOTE: This can be skipped if there are no unmatched elements (i.e., `matchedCount` + // equals `i`), unless we didn't visit _any_ elements in the above loop because we have + // no element matchers and no seed. + // Incrementing an initially-string "0" `i` allows `i` to remain a string only in that + // case, which will result in a "00" `matchedCount` that differs from `i` but is also + // numerically zero. if ( bySet && i !== matchedCount ) { j = 0; while ( (matcher = setMatchers[j++]) ) { @@ -2807,7 +2502,7 @@ function matcherFromGroupMatchers( elementMatchers, setMatchers ) { superMatcher; } -compile = Sizzle.compile = function( selector, group /* Internal Use Only */ ) { +compile = Sizzle.compile = function( selector, match /* Internal Use Only */ ) { var i, setMatchers = [], elementMatchers = [], @@ -2815,12 +2510,12 @@ compile = Sizzle.compile = function( selector, group /* Internal Use Only */ ) { if ( !cached ) { // Generate a function of recursive functions that can be used to check each element - if ( !group ) { - group = tokenize( selector ); + if ( !match ) { + match = tokenize( selector ); } - i = group.length; + i = match.length; while ( i-- ) { - cached = matcherFromTokens( group[i] ); + cached = matcherFromTokens( match[i] ); if ( cached[ expando ] ) { setMatchers.push( cached ); } else { @@ -2830,91 +2525,101 @@ compile = Sizzle.compile = function( selector, group /* Internal Use Only */ ) { // Cache the compiled function cached = compilerCache( selector, matcherFromGroupMatchers( elementMatchers, setMatchers ) ); + + // Save selector and tokenization + cached.selector = selector; } return cached; }; -function multipleContexts( selector, contexts, results ) { - var i = 0, - len = contexts.length; - for ( ; i < len; i++ ) { - Sizzle( selector, contexts[i], results ); - } - return results; -} - -function select( selector, context, results, seed ) { +/** + * A low-level selection function that works with Sizzle's compiled + * selector functions + * @param {String|Function} selector A selector or a pre-compiled + * selector function built with Sizzle.compile + * @param {Element} context + * @param {Array} [results] + * @param {Array} [seed] A set of elements to match against + */ +select = Sizzle.select = function( selector, context, results, seed ) { var i, tokens, token, type, find, - match = tokenize( selector ); + compiled = typeof selector === "function" && selector, + match = !seed && tokenize( (selector = compiled.selector || selector) ); - if ( !seed ) { - // Try to minimize operations if there is only one group - if ( match.length === 1 ) { + results = results || []; - // Take a shortcut and set the context if the root selector is an ID - tokens = match[0] = match[0].slice( 0 ); - if ( tokens.length > 2 && (token = tokens[0]).type === "ID" && - support.getById && context.nodeType === 9 && documentIsHTML && - Expr.relative[ tokens[1].type ] ) { + // Try to minimize operations if there is only one selector in the list and no seed + // (the latter of which guarantees us context) + if ( match.length === 1 ) { - context = ( Expr.find["ID"]( token.matches[0].replace(runescape, funescape), context ) || [] )[0]; - if ( !context ) { - return results; - } - selector = selector.slice( tokens.shift().value.length ); + // Reduce context if the leading compound selector is an ID + tokens = match[0] = match[0].slice( 0 ); + if ( tokens.length > 2 && (token = tokens[0]).type === "ID" && + support.getById && context.nodeType === 9 && documentIsHTML && + Expr.relative[ tokens[1].type ] ) { + + context = ( Expr.find["ID"]( token.matches[0].replace(runescape, funescape), context ) || [] )[0]; + if ( !context ) { + return results; + + // Precompiled matchers will still verify ancestry, so step up a level + } else if ( compiled ) { + context = context.parentNode; } - // Fetch a seed set for right-to-left matching - i = matchExpr["needsContext"].test( selector ) ? 0 : tokens.length; - while ( i-- ) { - token = tokens[i]; + selector = selector.slice( tokens.shift().value.length ); + } - // Abort if we hit a combinator - if ( Expr.relative[ (type = token.type) ] ) { - break; - } - if ( (find = Expr.find[ type ]) ) { - // Search, expanding context for leading sibling combinators - if ( (seed = find( - token.matches[0].replace( runescape, funescape ), - rsibling.test( tokens[0].type ) && context.parentNode || context - )) ) { + // Fetch a seed set for right-to-left matching + i = matchExpr["needsContext"].test( selector ) ? 0 : tokens.length; + while ( i-- ) { + token = tokens[i]; - // If seed is empty or no tokens remain, we can return early - tokens.splice( i, 1 ); - selector = seed.length && toSelector( tokens ); - if ( !selector ) { - push.apply( results, seed ); - return results; - } + // Abort if we hit a combinator + if ( Expr.relative[ (type = token.type) ] ) { + break; + } + if ( (find = Expr.find[ type ]) ) { + // Search, expanding context for leading sibling combinators + if ( (seed = find( + token.matches[0].replace( runescape, funescape ), + rsibling.test( tokens[0].type ) && testContext( context.parentNode ) || context + )) ) { - break; + // If seed is empty or no tokens remain, we can return early + tokens.splice( i, 1 ); + selector = seed.length && toSelector( tokens ); + if ( !selector ) { + push.apply( results, seed ); + return results; } + + break; } } } } - // Compile and execute a filtering function + // Compile and execute a filtering function if one is not provided // Provide `match` to avoid retokenization if we modified the selector above - compile( selector, match )( + ( compiled || compile( selector, match ) )( seed, context, !documentIsHTML, results, - rsibling.test( selector ) + !context || rsibling.test( selector ) && testContext( context.parentNode ) || context ); return results; -} +}; // One-time assignments // Sort stability support.sortStable = expando.split("").sort( sortOrder ).join("") === expando; -// Support: Chrome<14 +// Support: Chrome 14-35+ // Always assume duplicates if they aren't passed to the comparison function -support.detectDuplicates = hasDuplicate; +support.detectDuplicates = !!hasDuplicate; // Initialize against the default document setDocument(); @@ -2962,32 +2667,456 @@ if ( !assert(function( div ) { addHandle( booleans, function( elem, name, isXML ) { var val; if ( !isXML ) { - return (val = elem.getAttributeNode( name )) && val.specified ? - val.value : - elem[ name ] === true ? name.toLowerCase() : null; + return elem[ name ] === true ? name.toLowerCase() : + (val = elem.getAttributeNode( name )) && val.specified ? + val.value : + null; } }); } +return Sizzle; + +})( window ); + + + jQuery.find = Sizzle; jQuery.expr = Sizzle.selectors; -jQuery.expr[":"] = jQuery.expr.pseudos; -jQuery.unique = Sizzle.uniqueSort; +jQuery.expr[ ":" ] = jQuery.expr.pseudos; +jQuery.uniqueSort = jQuery.unique = Sizzle.uniqueSort; jQuery.text = Sizzle.getText; jQuery.isXMLDoc = Sizzle.isXML; jQuery.contains = Sizzle.contains; -})( window ); -// String to Object options format cache -var optionsCache = {}; -// Convert String-formatted options into Object-formatted ones and store in cache +var dir = function( elem, dir, until ) { + var matched = [], + truncate = until !== undefined; + + while ( ( elem = elem[ dir ] ) && elem.nodeType !== 9 ) { + if ( elem.nodeType === 1 ) { + if ( truncate && jQuery( elem ).is( until ) ) { + break; + } + matched.push( elem ); + } + } + return matched; +}; + + +var siblings = function( n, elem ) { + var matched = []; + + for ( ; n; n = n.nextSibling ) { + if ( n.nodeType === 1 && n !== elem ) { + matched.push( n ); + } + } + + return matched; +}; + + +var rneedsContext = jQuery.expr.match.needsContext; + +var rsingleTag = ( /^<([\w-]+)\s*\/?>(?:<\/\1>|)$/ ); + + + +var risSimple = /^.[^:#\[\.,]*$/; + +// Implement the identical functionality for filter and not +function winnow( elements, qualifier, not ) { + if ( jQuery.isFunction( qualifier ) ) { + return jQuery.grep( elements, function( elem, i ) { + /* jshint -W018 */ + return !!qualifier.call( elem, i, elem ) !== not; + } ); + + } + + if ( qualifier.nodeType ) { + return jQuery.grep( elements, function( elem ) { + return ( elem === qualifier ) !== not; + } ); + + } + + if ( typeof qualifier === "string" ) { + if ( risSimple.test( qualifier ) ) { + return jQuery.filter( qualifier, elements, not ); + } + + qualifier = jQuery.filter( qualifier, elements ); + } + + return jQuery.grep( elements, function( elem ) { + return ( indexOf.call( qualifier, elem ) > -1 ) !== not; + } ); +} + +jQuery.filter = function( expr, elems, not ) { + var elem = elems[ 0 ]; + + if ( not ) { + expr = ":not(" + expr + ")"; + } + + return elems.length === 1 && elem.nodeType === 1 ? + jQuery.find.matchesSelector( elem, expr ) ? [ elem ] : [] : + jQuery.find.matches( expr, jQuery.grep( elems, function( elem ) { + return elem.nodeType === 1; + } ) ); +}; + +jQuery.fn.extend( { + find: function( selector ) { + var i, + len = this.length, + ret = [], + self = this; + + if ( typeof selector !== "string" ) { + return this.pushStack( jQuery( selector ).filter( function() { + for ( i = 0; i < len; i++ ) { + if ( jQuery.contains( self[ i ], this ) ) { + return true; + } + } + } ) ); + } + + for ( i = 0; i < len; i++ ) { + jQuery.find( selector, self[ i ], ret ); + } + + // Needed because $( selector, context ) becomes $( context ).find( selector ) + ret = this.pushStack( len > 1 ? jQuery.unique( ret ) : ret ); + ret.selector = this.selector ? this.selector + " " + selector : selector; + return ret; + }, + filter: function( selector ) { + return this.pushStack( winnow( this, selector || [], false ) ); + }, + not: function( selector ) { + return this.pushStack( winnow( this, selector || [], true ) ); + }, + is: function( selector ) { + return !!winnow( + this, + + // If this is a positional/relative selector, check membership in the returned set + // so $("p:first").is("p:last") won't return true for a doc with two "p". + typeof selector === "string" && rneedsContext.test( selector ) ? + jQuery( selector ) : + selector || [], + false + ).length; + } +} ); + + +// Initialize a jQuery object + + +// A central reference to the root jQuery(document) +var rootjQuery, + + // A simple way to check for HTML strings + // Prioritize #id over to avoid XSS via location.hash (#9521) + // Strict HTML recognition (#11290: must start with <) + rquickExpr = /^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]*))$/, + + init = jQuery.fn.init = function( selector, context, root ) { + var match, elem; + + // HANDLE: $(""), $(null), $(undefined), $(false) + if ( !selector ) { + return this; + } + + // Method init() accepts an alternate rootjQuery + // so migrate can support jQuery.sub (gh-2101) + root = root || rootjQuery; + + // Handle HTML strings + if ( typeof selector === "string" ) { + if ( selector[ 0 ] === "<" && + selector[ selector.length - 1 ] === ">" && + selector.length >= 3 ) { + + // Assume that strings that start and end with <> are HTML and skip the regex check + match = [ null, selector, null ]; + + } else { + match = rquickExpr.exec( selector ); + } + + // Match html or make sure no context is specified for #id + if ( match && ( match[ 1 ] || !context ) ) { + + // HANDLE: $(html) -> $(array) + if ( match[ 1 ] ) { + context = context instanceof jQuery ? context[ 0 ] : context; + + // Option to run scripts is true for back-compat + // Intentionally let the error be thrown if parseHTML is not present + jQuery.merge( this, jQuery.parseHTML( + match[ 1 ], + context && context.nodeType ? context.ownerDocument || context : document, + true + ) ); + + // HANDLE: $(html, props) + if ( rsingleTag.test( match[ 1 ] ) && jQuery.isPlainObject( context ) ) { + for ( match in context ) { + + // Properties of context are called as methods if possible + if ( jQuery.isFunction( this[ match ] ) ) { + this[ match ]( context[ match ] ); + + // ...and otherwise set as attributes + } else { + this.attr( match, context[ match ] ); + } + } + } + + return this; + + // HANDLE: $(#id) + } else { + elem = document.getElementById( match[ 2 ] ); + + // Support: Blackberry 4.6 + // gEBID returns nodes no longer in the document (#6963) + if ( elem && elem.parentNode ) { + + // Inject the element directly into the jQuery object + this.length = 1; + this[ 0 ] = elem; + } + + this.context = document; + this.selector = selector; + return this; + } + + // HANDLE: $(expr, $(...)) + } else if ( !context || context.jquery ) { + return ( context || root ).find( selector ); + + // HANDLE: $(expr, context) + // (which is just equivalent to: $(context).find(expr) + } else { + return this.constructor( context ).find( selector ); + } + + // HANDLE: $(DOMElement) + } else if ( selector.nodeType ) { + this.context = this[ 0 ] = selector; + this.length = 1; + return this; + + // HANDLE: $(function) + // Shortcut for document ready + } else if ( jQuery.isFunction( selector ) ) { + return root.ready !== undefined ? + root.ready( selector ) : + + // Execute immediately if ready is not present + selector( jQuery ); + } + + if ( selector.selector !== undefined ) { + this.selector = selector.selector; + this.context = selector.context; + } + + return jQuery.makeArray( selector, this ); + }; + +// Give the init function the jQuery prototype for later instantiation +init.prototype = jQuery.fn; + +// Initialize central reference +rootjQuery = jQuery( document ); + + +var rparentsprev = /^(?:parents|prev(?:Until|All))/, + + // Methods guaranteed to produce a unique set when starting from a unique set + guaranteedUnique = { + children: true, + contents: true, + next: true, + prev: true + }; + +jQuery.fn.extend( { + has: function( target ) { + var targets = jQuery( target, this ), + l = targets.length; + + return this.filter( function() { + var i = 0; + for ( ; i < l; i++ ) { + if ( jQuery.contains( this, targets[ i ] ) ) { + return true; + } + } + } ); + }, + + closest: function( selectors, context ) { + var cur, + i = 0, + l = this.length, + matched = [], + pos = rneedsContext.test( selectors ) || typeof selectors !== "string" ? + jQuery( selectors, context || this.context ) : + 0; + + for ( ; i < l; i++ ) { + for ( cur = this[ i ]; cur && cur !== context; cur = cur.parentNode ) { + + // Always skip document fragments + if ( cur.nodeType < 11 && ( pos ? + pos.index( cur ) > -1 : + + // Don't pass non-elements to Sizzle + cur.nodeType === 1 && + jQuery.find.matchesSelector( cur, selectors ) ) ) { + + matched.push( cur ); + break; + } + } + } + + return this.pushStack( matched.length > 1 ? jQuery.uniqueSort( matched ) : matched ); + }, + + // Determine the position of an element within the set + index: function( elem ) { + + // No argument, return index in parent + if ( !elem ) { + return ( this[ 0 ] && this[ 0 ].parentNode ) ? this.first().prevAll().length : -1; + } + + // Index in selector + if ( typeof elem === "string" ) { + return indexOf.call( jQuery( elem ), this[ 0 ] ); + } + + // Locate the position of the desired element + return indexOf.call( this, + + // If it receives a jQuery object, the first element is used + elem.jquery ? elem[ 0 ] : elem + ); + }, + + add: function( selector, context ) { + return this.pushStack( + jQuery.uniqueSort( + jQuery.merge( this.get(), jQuery( selector, context ) ) + ) + ); + }, + + addBack: function( selector ) { + return this.add( selector == null ? + this.prevObject : this.prevObject.filter( selector ) + ); + } +} ); + +function sibling( cur, dir ) { + while ( ( cur = cur[ dir ] ) && cur.nodeType !== 1 ) {} + return cur; +} + +jQuery.each( { + parent: function( elem ) { + var parent = elem.parentNode; + return parent && parent.nodeType !== 11 ? parent : null; + }, + parents: function( elem ) { + return dir( elem, "parentNode" ); + }, + parentsUntil: function( elem, i, until ) { + return dir( elem, "parentNode", until ); + }, + next: function( elem ) { + return sibling( elem, "nextSibling" ); + }, + prev: function( elem ) { + return sibling( elem, "previousSibling" ); + }, + nextAll: function( elem ) { + return dir( elem, "nextSibling" ); + }, + prevAll: function( elem ) { + return dir( elem, "previousSibling" ); + }, + nextUntil: function( elem, i, until ) { + return dir( elem, "nextSibling", until ); + }, + prevUntil: function( elem, i, until ) { + return dir( elem, "previousSibling", until ); + }, + siblings: function( elem ) { + return siblings( ( elem.parentNode || {} ).firstChild, elem ); + }, + children: function( elem ) { + return siblings( elem.firstChild ); + }, + contents: function( elem ) { + return elem.contentDocument || jQuery.merge( [], elem.childNodes ); + } +}, function( name, fn ) { + jQuery.fn[ name ] = function( until, selector ) { + var matched = jQuery.map( this, fn, until ); + + if ( name.slice( -5 ) !== "Until" ) { + selector = until; + } + + if ( selector && typeof selector === "string" ) { + matched = jQuery.filter( selector, matched ); + } + + if ( this.length > 1 ) { + + // Remove duplicates + if ( !guaranteedUnique[ name ] ) { + jQuery.uniqueSort( matched ); + } + + // Reverse order for parents* and prev-derivatives + if ( rparentsprev.test( name ) ) { + matched.reverse(); + } + } + + return this.pushStack( matched ); + }; +} ); +var rnotwhite = ( /\S+/g ); + + + +// Convert String-formatted options into Object-formatted ones function createOptions( options ) { - var object = optionsCache[ options ] = {}; - jQuery.each( options.match( core_rnotwhite ) || [], function( _, flag ) { + var object = {}; + jQuery.each( options.match( rnotwhite ) || [], function( _, flag ) { object[ flag ] = true; - }); + } ); return object; } @@ -3018,156 +3147,186 @@ jQuery.Callbacks = function( options ) { // Convert options from String-formatted to Object-formatted if needed // (we check in cache first) options = typeof options === "string" ? - ( optionsCache[ options ] || createOptions( options ) ) : + createOptions( options ) : jQuery.extend( {}, options ); var // Flag to know if list is currently firing firing, - // Last fire value (for non-forgettable lists) + + // Last fire value for non-forgettable lists memory, + // Flag to know if list was already fired fired, - // End of the loop when firing - firingLength, - // Index of currently firing callback (modified by remove if needed) - firingIndex, - // First callback to fire (used internally by add and fireWith) - firingStart, + + // Flag to prevent firing + locked, + // Actual callback list list = [], - // Stack of fire calls for repeatable lists - stack = !options.once && [], + + // Queue of execution data for repeatable lists + queue = [], + + // Index of currently firing callback (modified by add/remove as needed) + firingIndex = -1, + // Fire callbacks - fire = function( data ) { - memory = options.memory && data; - fired = true; - firingIndex = firingStart || 0; - firingStart = 0; - firingLength = list.length; - firing = true; - for ( ; list && firingIndex < firingLength; firingIndex++ ) { - if ( list[ firingIndex ].apply( data[ 0 ], data[ 1 ] ) === false && options.stopOnFalse ) { - memory = false; // To prevent further calls using add - break; + fire = function() { + + // Enforce single-firing + locked = options.once; + + // Execute callbacks for all pending executions, + // respecting firingIndex overrides and runtime changes + fired = firing = true; + for ( ; queue.length; firingIndex = -1 ) { + memory = queue.shift(); + while ( ++firingIndex < list.length ) { + + // Run callback and check for early termination + if ( list[ firingIndex ].apply( memory[ 0 ], memory[ 1 ] ) === false && + options.stopOnFalse ) { + + // Jump to end and forget the data so .add doesn't re-fire + firingIndex = list.length; + memory = false; + } } } + + // Forget the data if we're done with it + if ( !options.memory ) { + memory = false; + } + firing = false; - if ( list ) { - if ( stack ) { - if ( stack.length ) { - fire( stack.shift() ); - } - } else if ( memory ) { + + // Clean up if we're done firing for good + if ( locked ) { + + // Keep an empty list if we have data for future add calls + if ( memory ) { list = []; + + // Otherwise, this object is spent } else { - self.disable(); + list = ""; } } }, + // Actual Callbacks object self = { + // Add a callback or a collection of callbacks to the list add: function() { if ( list ) { - // First, we save the current length - var start = list.length; - (function add( args ) { + + // If we have memory from a past run, we should fire after adding + if ( memory && !firing ) { + firingIndex = list.length - 1; + queue.push( memory ); + } + + ( function add( args ) { jQuery.each( args, function( _, arg ) { - var type = jQuery.type( arg ); - if ( type === "function" ) { + if ( jQuery.isFunction( arg ) ) { if ( !options.unique || !self.has( arg ) ) { list.push( arg ); } - } else if ( arg && arg.length && type !== "string" ) { + } else if ( arg && arg.length && jQuery.type( arg ) !== "string" ) { + // Inspect recursively add( arg ); } - }); - })( arguments ); - // Do we need to add the callbacks to the - // current firing batch? - if ( firing ) { - firingLength = list.length; - // With memory, if we're not firing then - // we should call right away - } else if ( memory ) { - firingStart = start; - fire( memory ); + } ); + } )( arguments ); + + if ( memory && !firing ) { + fire(); } } return this; }, + // Remove a callback from the list remove: function() { - if ( list ) { - jQuery.each( arguments, function( _, arg ) { - var index; - while( ( index = jQuery.inArray( arg, list, index ) ) > -1 ) { - list.splice( index, 1 ); - // Handle firing indexes - if ( firing ) { - if ( index <= firingLength ) { - firingLength--; - } - if ( index <= firingIndex ) { - firingIndex--; - } - } + jQuery.each( arguments, function( _, arg ) { + var index; + while ( ( index = jQuery.inArray( arg, list, index ) ) > -1 ) { + list.splice( index, 1 ); + + // Handle firing indexes + if ( index <= firingIndex ) { + firingIndex--; } - }); - } + } + } ); return this; }, + // Check if a given callback is in the list. // If no argument is given, return whether or not list has callbacks attached. has: function( fn ) { - return fn ? jQuery.inArray( fn, list ) > -1 : !!( list && list.length ); + return fn ? + jQuery.inArray( fn, list ) > -1 : + list.length > 0; }, + // Remove all callbacks from the list empty: function() { - list = []; - firingLength = 0; - return this; - }, - // Have the list do nothing anymore - disable: function() { - list = stack = memory = undefined; - return this; - }, - // Is it disabled? - disabled: function() { - return !list; - }, - // Lock the list in its current state - lock: function() { - stack = undefined; - if ( !memory ) { - self.disable(); + if ( list ) { + list = []; } return this; }, - // Is it locked? - locked: function() { - return !stack; + + // Disable .fire and .add + // Abort any current/pending executions + // Clear all callbacks and values + disable: function() { + locked = queue = []; + list = memory = ""; + return this; }, + disabled: function() { + return !list; + }, + + // Disable .fire + // Also disable .add unless we have memory (since it would have no effect) + // Abort any pending executions + lock: function() { + locked = queue = []; + if ( !memory ) { + list = memory = ""; + } + return this; + }, + locked: function() { + return !!locked; + }, + // Call all callbacks with the given context and arguments fireWith: function( context, args ) { - if ( list && ( !fired || stack ) ) { + if ( !locked ) { args = args || []; args = [ context, args.slice ? args.slice() : args ]; - if ( firing ) { - stack.push( args ); - } else { - fire( args ); + queue.push( args ); + if ( !firing ) { + fire(); } } return this; }, + // Call all the callbacks with the given arguments fire: function() { self.fireWith( this, arguments ); return this; }, + // To know if the callbacks have already been called at least once fired: function() { return !!fired; @@ -3176,14 +3335,17 @@ jQuery.Callbacks = function( options ) { return self; }; -jQuery.extend({ + + +jQuery.extend( { Deferred: function( func ) { var tuples = [ + // action, add listener, listener list, final state - [ "resolve", "done", jQuery.Callbacks("once memory"), "resolved" ], - [ "reject", "fail", jQuery.Callbacks("once memory"), "rejected" ], - [ "notify", "progress", jQuery.Callbacks("memory") ] + [ "resolve", "done", jQuery.Callbacks( "once memory" ), "resolved" ], + [ "reject", "fail", jQuery.Callbacks( "once memory" ), "rejected" ], + [ "notify", "progress", jQuery.Callbacks( "memory" ) ] ], state = "pending", promise = { @@ -3196,26 +3358,30 @@ jQuery.extend({ }, then: function( /* fnDone, fnFail, fnProgress */ ) { var fns = arguments; - return jQuery.Deferred(function( newDefer ) { + return jQuery.Deferred( function( newDefer ) { jQuery.each( tuples, function( i, tuple ) { - var action = tuple[ 0 ], - fn = jQuery.isFunction( fns[ i ] ) && fns[ i ]; + var fn = jQuery.isFunction( fns[ i ] ) && fns[ i ]; + // deferred[ done | fail | progress ] for forwarding actions to newDefer - deferred[ tuple[1] ](function() { + deferred[ tuple[ 1 ] ]( function() { var returned = fn && fn.apply( this, arguments ); if ( returned && jQuery.isFunction( returned.promise ) ) { returned.promise() + .progress( newDefer.notify ) .done( newDefer.resolve ) - .fail( newDefer.reject ) - .progress( newDefer.notify ); + .fail( newDefer.reject ); } else { - newDefer[ action + "With" ]( this === promise ? newDefer.promise() : this, fn ? [ returned ] : arguments ); + newDefer[ tuple[ 0 ] + "With" ]( + this === promise ? newDefer.promise() : this, + fn ? [ returned ] : arguments + ); } - }); - }); + } ); + } ); fns = null; - }).promise(); + } ).promise(); }, + // Get a promise for this deferred // If obj is provided, the promise aspect is added to the object promise: function( obj ) { @@ -3233,11 +3399,12 @@ jQuery.extend({ stateString = tuple[ 3 ]; // promise[ done | fail | progress ] = list.add - promise[ tuple[1] ] = list.add; + promise[ tuple[ 1 ] ] = list.add; // Handle state if ( stateString ) { - list.add(function() { + list.add( function() { + // state = [ resolved | rejected ] state = stateString; @@ -3246,12 +3413,12 @@ jQuery.extend({ } // deferred[ resolve | reject | notify ] - deferred[ tuple[0] ] = function() { - deferred[ tuple[0] + "With" ]( this === deferred ? promise : this, arguments ); + deferred[ tuple[ 0 ] ] = function() { + deferred[ tuple[ 0 ] + "With" ]( this === deferred ? promise : this, arguments ); return this; }; - deferred[ tuple[0] + "With" ] = list.fireWith; - }); + deferred[ tuple[ 0 ] + "With" ] = list.fireWith; + } ); // Make the deferred a promise promise.promise( deferred ); @@ -3268,21 +3435,23 @@ jQuery.extend({ // Deferred helper when: function( subordinate /* , ..., subordinateN */ ) { var i = 0, - resolveValues = core_slice.call( arguments ), + resolveValues = slice.call( arguments ), length = resolveValues.length, // the count of uncompleted subordinates - remaining = length !== 1 || ( subordinate && jQuery.isFunction( subordinate.promise ) ) ? length : 0, + remaining = length !== 1 || + ( subordinate && jQuery.isFunction( subordinate.promise ) ) ? length : 0, - // the master Deferred. If resolveValues consist of only a single Deferred, just use that. + // the master Deferred. + // If resolveValues consist of only a single Deferred, just use that. deferred = remaining === 1 ? subordinate : jQuery.Deferred(), // Update function for both resolve and progress values updateFunc = function( i, contexts, values ) { return function( value ) { contexts[ i ] = this; - values[ i ] = arguments.length > 1 ? core_slice.call( arguments ) : value; - if( values === progressValues ) { + values[ i ] = arguments.length > 1 ? slice.call( arguments ) : value; + if ( values === progressValues ) { deferred.notifyWith( contexts, values ); } else if ( !( --remaining ) ) { deferred.resolveWith( contexts, values ); @@ -3292,7 +3461,7 @@ jQuery.extend({ progressValues, progressContexts, resolveContexts; - // add listeners to Deferred subordinates; treat others as resolved + // Add listeners to Deferred subordinates; treat others as resolved if ( length > 1 ) { progressValues = new Array( length ); progressContexts = new Array( length ); @@ -3300,526 +3469,485 @@ jQuery.extend({ for ( ; i < length; i++ ) { if ( resolveValues[ i ] && jQuery.isFunction( resolveValues[ i ].promise ) ) { resolveValues[ i ].promise() + .progress( updateFunc( i, progressContexts, progressValues ) ) .done( updateFunc( i, resolveContexts, resolveValues ) ) - .fail( deferred.reject ) - .progress( updateFunc( i, progressContexts, progressValues ) ); + .fail( deferred.reject ); } else { --remaining; } } } - // if we're not waiting on anything, resolve the master + // If we're not waiting on anything, resolve the master if ( !remaining ) { deferred.resolveWith( resolveContexts, resolveValues ); } return deferred.promise(); } -}); -jQuery.support = (function( support ) { +} ); - var all, a, input, select, fragment, opt, eventName, isSupported, i, - div = document.createElement("div"); - // Setup - div.setAttribute( "className", "t" ); - div.innerHTML = "
        a"; +// The deferred used on DOM ready +var readyList; - // Finish early in limited (non-browser) environments - all = div.getElementsByTagName("*") || []; - a = div.getElementsByTagName("a")[ 0 ]; - if ( !a || !a.style || !all.length ) { - return support; - } +jQuery.fn.ready = function( fn ) { - // First batch of tests - select = document.createElement("select"); - opt = select.appendChild( document.createElement("option") ); - input = div.getElementsByTagName("input")[ 0 ]; + // Add the callback + jQuery.ready.promise().done( fn ); - a.style.cssText = "top:1px;float:left;opacity:.5"; + return this; +}; - // Test setAttribute on camelCase class. If it works, we need attrFixes when doing get/setAttribute (ie6/7) - support.getSetAttribute = div.className !== "t"; +jQuery.extend( { - // IE strips leading whitespace when .innerHTML is used - support.leadingWhitespace = div.firstChild.nodeType === 3; + // Is the DOM ready to be used? Set to true once it occurs. + isReady: false, - // Make sure that tbody elements aren't automatically inserted - // IE will insert them into empty tables - support.tbody = !div.getElementsByTagName("tbody").length; + // A counter to track how many items to wait for before + // the ready event fires. See #6781 + readyWait: 1, - // Make sure that link elements get serialized correctly by innerHTML - // This requires a wrapper element in IE - support.htmlSerialize = !!div.getElementsByTagName("link").length; + // Hold (or release) the ready event + holdReady: function( hold ) { + if ( hold ) { + jQuery.readyWait++; + } else { + jQuery.ready( true ); + } + }, - // Get the style information from getAttribute - // (IE uses .cssText instead) - support.style = /top/.test( a.getAttribute("style") ); + // Handle when the DOM is ready + ready: function( wait ) { - // Make sure that URLs aren't manipulated - // (IE normalizes it by default) - support.hrefNormalized = a.getAttribute("href") === "/a"; - - // Make sure that element opacity exists - // (IE uses filter instead) - // Use a regex to work around a WebKit issue. See #5145 - support.opacity = /^0.5/.test( a.style.opacity ); - - // Verify style float existence - // (IE uses styleFloat instead of cssFloat) - support.cssFloat = !!a.style.cssFloat; - - // Check the default checkbox/radio value ("" on WebKit; "on" elsewhere) - support.checkOn = !!input.value; - - // Make sure that a selected-by-default option has a working selected property. - // (WebKit defaults to false instead of true, IE too, if it's in an optgroup) - support.optSelected = opt.selected; - - // Tests for enctype support on a form (#6743) - support.enctype = !!document.createElement("form").enctype; - - // Makes sure cloning an html5 element does not cause problems - // Where outerHTML is undefined, this still works - support.html5Clone = document.createElement("nav").cloneNode( true ).outerHTML !== "<:nav>"; - - // Will be defined later - support.inlineBlockNeedsLayout = false; - support.shrinkWrapBlocks = false; - support.pixelPosition = false; - support.deleteExpando = true; - support.noCloneEvent = true; - support.reliableMarginRight = true; - support.boxSizingReliable = true; - - // Make sure checked status is properly cloned - input.checked = true; - support.noCloneChecked = input.cloneNode( true ).checked; - - // Make sure that the options inside disabled selects aren't marked as disabled - // (WebKit marks them as disabled) - select.disabled = true; - support.optDisabled = !opt.disabled; - - // Support: IE<9 - try { - delete div.test; - } catch( e ) { - support.deleteExpando = false; - } - - // Check if we can trust getAttribute("value") - input = document.createElement("input"); - input.setAttribute( "value", "" ); - support.input = input.getAttribute( "value" ) === ""; - - // Check if an input maintains its value after becoming a radio - input.value = "t"; - input.setAttribute( "type", "radio" ); - support.radioValue = input.value === "t"; - - // #11217 - WebKit loses check when the name is after the checked attribute - input.setAttribute( "checked", "t" ); - input.setAttribute( "name", "t" ); - - fragment = document.createDocumentFragment(); - fragment.appendChild( input ); - - // Check if a disconnected checkbox will retain its checked - // value of true after appended to the DOM (IE6/7) - support.appendChecked = input.checked; - - // WebKit doesn't clone checked state correctly in fragments - support.checkClone = fragment.cloneNode( true ).cloneNode( true ).lastChild.checked; - - // Support: IE<9 - // Opera does not clone events (and typeof div.attachEvent === undefined). - // IE9-10 clones events bound via attachEvent, but they don't trigger with .click() - if ( div.attachEvent ) { - div.attachEvent( "onclick", function() { - support.noCloneEvent = false; - }); - - div.cloneNode( true ).click(); - } - - // Support: IE<9 (lack submit/change bubble), Firefox 17+ (lack focusin event) - // Beware of CSP restrictions (https://developer.mozilla.org/en/Security/CSP) - for ( i in { submit: true, change: true, focusin: true }) { - div.setAttribute( eventName = "on" + i, "t" ); - - support[ i + "Bubbles" ] = eventName in window || div.attributes[ eventName ].expando === false; - } - - div.style.backgroundClip = "content-box"; - div.cloneNode( true ).style.backgroundClip = ""; - support.clearCloneStyle = div.style.backgroundClip === "content-box"; - - // Support: IE<9 - // Iteration over object's inherited properties before its own. - for ( i in jQuery( support ) ) { - break; - } - support.ownLast = i !== "0"; - - // Run tests that need a body at doc ready - jQuery(function() { - var container, marginDiv, tds, - divReset = "padding:0;margin:0;border:0;display:block;box-sizing:content-box;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;", - body = document.getElementsByTagName("body")[0]; - - if ( !body ) { - // Return for frameset docs that don't have a body + // Abort if there are pending holds or we're already ready + if ( wait === true ? --jQuery.readyWait : jQuery.isReady ) { return; } - container = document.createElement("div"); - container.style.cssText = "border:0;width:0;height:0;position:absolute;top:0;left:-9999px;margin-top:1px"; + // Remember that the DOM is ready + jQuery.isReady = true; - body.appendChild( container ).appendChild( div ); - - // Support: IE8 - // Check if table cells still have offsetWidth/Height when they are set - // to display:none and there are still other visible table cells in a - // table row; if so, offsetWidth/Height are not reliable for use when - // determining if an element has been hidden directly using - // display:none (it is still safe to use offsets if a parent element is - // hidden; don safety goggles and see bug #4512 for more information). - div.innerHTML = "
        t
        "; - tds = div.getElementsByTagName("td"); - tds[ 0 ].style.cssText = "padding:0;margin:0;border:0;display:none"; - isSupported = ( tds[ 0 ].offsetHeight === 0 ); - - tds[ 0 ].style.display = ""; - tds[ 1 ].style.display = "none"; - - // Support: IE8 - // Check if empty table cells still have offsetWidth/Height - support.reliableHiddenOffsets = isSupported && ( tds[ 0 ].offsetHeight === 0 ); - - // Check box-sizing and margin behavior. - div.innerHTML = ""; - div.style.cssText = "box-sizing:border-box;-moz-box-sizing:border-box;-webkit-box-sizing:border-box;padding:1px;border:1px;display:block;width:4px;margin-top:1%;position:absolute;top:1%;"; - - // Workaround failing boxSizing test due to offsetWidth returning wrong value - // with some non-1 values of body zoom, ticket #13543 - jQuery.swap( body, body.style.zoom != null ? { zoom: 1 } : {}, function() { - support.boxSizing = div.offsetWidth === 4; - }); - - // Use window.getComputedStyle because jsdom on node.js will break without it. - if ( window.getComputedStyle ) { - support.pixelPosition = ( window.getComputedStyle( div, null ) || {} ).top !== "1%"; - support.boxSizingReliable = ( window.getComputedStyle( div, null ) || { width: "4px" } ).width === "4px"; - - // Check if div with explicit width and no margin-right incorrectly - // gets computed margin-right based on width of container. (#3333) - // Fails in WebKit before Feb 2011 nightlies - // WebKit Bug 13343 - getComputedStyle returns wrong value for margin-right - marginDiv = div.appendChild( document.createElement("div") ); - marginDiv.style.cssText = div.style.cssText = divReset; - marginDiv.style.marginRight = marginDiv.style.width = "0"; - div.style.width = "1px"; - - support.reliableMarginRight = - !parseFloat( ( window.getComputedStyle( marginDiv, null ) || {} ).marginRight ); + // If a normal DOM Ready event fired, decrement, and wait if need be + if ( wait !== true && --jQuery.readyWait > 0 ) { + return; } - if ( typeof div.style.zoom !== core_strundefined ) { - // Support: IE<8 - // Check if natively block-level elements act like inline-block - // elements when setting their display to 'inline' and giving - // them layout - div.innerHTML = ""; - div.style.cssText = divReset + "width:1px;padding:1px;display:inline;zoom:1"; - support.inlineBlockNeedsLayout = ( div.offsetWidth === 3 ); + // If there are functions bound, to execute + readyList.resolveWith( document, [ jQuery ] ); - // Support: IE6 - // Check if elements with layout shrink-wrap their children - div.style.display = "block"; - div.innerHTML = "
        "; - div.firstChild.style.width = "5px"; - support.shrinkWrapBlocks = ( div.offsetWidth !== 3 ); + // Trigger any bound ready events + if ( jQuery.fn.triggerHandler ) { + jQuery( document ).triggerHandler( "ready" ); + jQuery( document ).off( "ready" ); + } + } +} ); - if ( support.inlineBlockNeedsLayout ) { - // Prevent IE 6 from affecting layout for positioned elements #11048 - // Prevent IE from shrinking the body in IE 7 mode #12869 - // Support: IE<8 - body.style.zoom = 1; +/** + * The ready event handler and self cleanup method + */ +function completed() { + document.removeEventListener( "DOMContentLoaded", completed ); + window.removeEventListener( "load", completed ); + jQuery.ready(); +} + +jQuery.ready.promise = function( obj ) { + if ( !readyList ) { + + readyList = jQuery.Deferred(); + + // Catch cases where $(document).ready() is called + // after the browser event has already occurred. + // Support: IE9-10 only + // Older IE sometimes signals "interactive" too soon + if ( document.readyState === "complete" || + ( document.readyState !== "loading" && !document.documentElement.doScroll ) ) { + + // Handle it asynchronously to allow scripts the opportunity to delay ready + window.setTimeout( jQuery.ready ); + + } else { + + // Use the handy event callback + document.addEventListener( "DOMContentLoaded", completed ); + + // A fallback to window.onload, that will always work + window.addEventListener( "load", completed ); + } + } + return readyList.promise( obj ); +}; + +// Kick off the DOM ready check even if the user does not +jQuery.ready.promise(); + + + + +// Multifunctional method to get and set values of a collection +// The value/s can optionally be executed if it's a function +var access = function( elems, fn, key, value, chainable, emptyGet, raw ) { + var i = 0, + len = elems.length, + bulk = key == null; + + // Sets many values + if ( jQuery.type( key ) === "object" ) { + chainable = true; + for ( i in key ) { + access( elems, fn, i, key[ i ], true, emptyGet, raw ); + } + + // Sets one value + } else if ( value !== undefined ) { + chainable = true; + + if ( !jQuery.isFunction( value ) ) { + raw = true; + } + + if ( bulk ) { + + // Bulk operations run against the entire set + if ( raw ) { + fn.call( elems, value ); + fn = null; + + // ...except when executing function values + } else { + bulk = fn; + fn = function( elem, key, value ) { + return bulk.call( jQuery( elem ), value ); + }; } } - body.removeChild( container ); - - // Null elements to avoid leaks in IE - container = div = tds = marginDiv = null; - }); - - // Null elements to avoid leaks in IE - all = select = fragment = opt = a = input = null; - - return support; -})({}); - -var rbrace = /(?:\{[\s\S]*\}|\[[\s\S]*\])$/, - rmultiDash = /([A-Z])/g; - -function internalData( elem, name, data, pvt /* Internal Use Only */ ){ - if ( !jQuery.acceptData( elem ) ) { - return; - } - - var ret, thisCache, - internalKey = jQuery.expando, - - // We have to handle DOM nodes and JS objects differently because IE6-7 - // can't GC object references properly across the DOM-JS boundary - isNode = elem.nodeType, - - // Only DOM nodes need the global jQuery cache; JS object data is - // attached directly to the object so GC can occur automatically - cache = isNode ? jQuery.cache : elem, - - // Only defining an ID for JS objects if its cache already exists allows - // the code to shortcut on the same path as a DOM node with no cache - id = isNode ? elem[ internalKey ] : elem[ internalKey ] && internalKey; - - // Avoid doing any more work than we need to when trying to get data on an - // object that has no data at all - if ( (!id || !cache[id] || (!pvt && !cache[id].data)) && data === undefined && typeof name === "string" ) { - return; - } - - if ( !id ) { - // Only DOM nodes need a new unique ID for each element since their data - // ends up in the global cache - if ( isNode ) { - id = elem[ internalKey ] = core_deletedIds.pop() || jQuery.guid++; - } else { - id = internalKey; + if ( fn ) { + for ( ; i < len; i++ ) { + fn( + elems[ i ], key, raw ? + value : + value.call( elems[ i ], i, fn( elems[ i ], key ) ) + ); + } } } - if ( !cache[ id ] ) { - // Avoid exposing jQuery metadata on plain JS objects when the object - // is serialized using JSON.stringify - cache[ id ] = isNode ? {} : { toJSON: jQuery.noop }; - } + return chainable ? + elems : - // An object can be passed to jQuery.data instead of a key/value pair; this gets - // shallow copied over onto the existing cache - if ( typeof name === "object" || typeof name === "function" ) { - if ( pvt ) { - cache[ id ] = jQuery.extend( cache[ id ], name ); - } else { - cache[ id ].data = jQuery.extend( cache[ id ].data, name ); - } - } + // Gets + bulk ? + fn.call( elems ) : + len ? fn( elems[ 0 ], key ) : emptyGet; +}; +var acceptData = function( owner ) { - thisCache = cache[ id ]; + // Accepts only: + // - Node + // - Node.ELEMENT_NODE + // - Node.DOCUMENT_NODE + // - Object + // - Any + /* jshint -W018 */ + return owner.nodeType === 1 || owner.nodeType === 9 || !( +owner.nodeType ); +}; - // jQuery data() is stored in a separate object inside the object's internal data - // cache in order to avoid key collisions between internal data and user-defined - // data. - if ( !pvt ) { - if ( !thisCache.data ) { - thisCache.data = {}; - } - thisCache = thisCache.data; - } - if ( data !== undefined ) { - thisCache[ jQuery.camelCase( name ) ] = data; - } - // Check for both converted-to-camel and non-converted data property names - // If a data property was specified - if ( typeof name === "string" ) { - - // First Try to find as-is property data - ret = thisCache[ name ]; - - // Test for null|undefined property data - if ( ret == null ) { - - // Try to find the camelCased property - ret = thisCache[ jQuery.camelCase( name ) ]; - } - } else { - ret = thisCache; - } - - return ret; +function Data() { + this.expando = jQuery.expando + Data.uid++; } -function internalRemoveData( elem, name, pvt ) { - if ( !jQuery.acceptData( elem ) ) { - return; - } +Data.uid = 1; - var thisCache, i, - isNode = elem.nodeType, +Data.prototype = { - // See jQuery.data for more information - cache = isNode ? jQuery.cache : elem, - id = isNode ? elem[ jQuery.expando ] : jQuery.expando; + register: function( owner, initial ) { + var value = initial || {}; - // If there is already no cache entry for this object, there is no - // purpose in continuing - if ( !cache[ id ] ) { - return; - } + // If it is a node unlikely to be stringify-ed or looped over + // use plain assignment + if ( owner.nodeType ) { + owner[ this.expando ] = value; - if ( name ) { + // Otherwise secure it in a non-enumerable, non-writable property + // configurability must be true to allow the property to be + // deleted with the delete operator + } else { + Object.defineProperty( owner, this.expando, { + value: value, + writable: true, + configurable: true + } ); + } + return owner[ this.expando ]; + }, + cache: function( owner ) { - thisCache = pvt ? cache[ id ] : cache[ id ].data; + // We can accept data for non-element nodes in modern browsers, + // but we should not, see #8335. + // Always return an empty object. + if ( !acceptData( owner ) ) { + return {}; + } - if ( thisCache ) { + // Check if the owner object already has a cache + var value = owner[ this.expando ]; - // Support array or space separated string names for data keys - if ( !jQuery.isArray( name ) ) { + // If not, create one + if ( !value ) { + value = {}; - // try the string as a key before any manipulation - if ( name in thisCache ) { - name = [ name ]; + // We can accept data for non-element nodes in modern browsers, + // but we should not, see #8335. + // Always return an empty object. + if ( acceptData( owner ) ) { + + // If it is a node unlikely to be stringify-ed or looped over + // use plain assignment + if ( owner.nodeType ) { + owner[ this.expando ] = value; + + // Otherwise secure it in a non-enumerable property + // configurable must be true to allow the property to be + // deleted when data is removed } else { - - // split the camel cased version by spaces unless a key with the spaces exists - name = jQuery.camelCase( name ); - if ( name in thisCache ) { - name = [ name ]; - } else { - name = name.split(" "); - } + Object.defineProperty( owner, this.expando, { + value: value, + configurable: true + } ); } - } else { + } + } + + return value; + }, + set: function( owner, data, value ) { + var prop, + cache = this.cache( owner ); + + // Handle: [ owner, key, value ] args + if ( typeof data === "string" ) { + cache[ data ] = value; + + // Handle: [ owner, { properties } ] args + } else { + + // Copy the properties one-by-one to the cache object + for ( prop in data ) { + cache[ prop ] = data[ prop ]; + } + } + return cache; + }, + get: function( owner, key ) { + return key === undefined ? + this.cache( owner ) : + owner[ this.expando ] && owner[ this.expando ][ key ]; + }, + access: function( owner, key, value ) { + var stored; + + // In cases where either: + // + // 1. No key was specified + // 2. A string key was specified, but no value provided + // + // Take the "read" path and allow the get method to determine + // which value to return, respectively either: + // + // 1. The entire cache object + // 2. The data stored at the key + // + if ( key === undefined || + ( ( key && typeof key === "string" ) && value === undefined ) ) { + + stored = this.get( owner, key ); + + return stored !== undefined ? + stored : this.get( owner, jQuery.camelCase( key ) ); + } + + // When the key is not a string, or both a key and value + // are specified, set or extend (existing objects) with either: + // + // 1. An object of properties + // 2. A key and value + // + this.set( owner, key, value ); + + // Since the "set" path can have two possible entry points + // return the expected data based on which path was taken[*] + return value !== undefined ? value : key; + }, + remove: function( owner, key ) { + var i, name, camel, + cache = owner[ this.expando ]; + + if ( cache === undefined ) { + return; + } + + if ( key === undefined ) { + this.register( owner ); + + } else { + + // Support array or space separated string of keys + if ( jQuery.isArray( key ) ) { + // If "name" is an array of keys... // When data is initially created, via ("key", "val") signature, // keys will be converted to camelCase. // Since there is no way to tell _how_ a key was added, remove // both plain key and camelCase key. #12786 // This will only penalize the array argument path. - name = name.concat( jQuery.map( name, jQuery.camelCase ) ); + name = key.concat( key.map( jQuery.camelCase ) ); + } else { + camel = jQuery.camelCase( key ); + + // Try the string as a key before any manipulation + if ( key in cache ) { + name = [ key, camel ]; + } else { + + // If a key with the spaces exists, use it. + // Otherwise, create an array by matching non-whitespace + name = camel; + name = name in cache ? + [ name ] : ( name.match( rnotwhite ) || [] ); + } } i = name.length; + while ( i-- ) { - delete thisCache[ name[i] ]; - } - - // If there is no data left in the cache, we want to continue - // and let the cache object itself get destroyed - if ( pvt ? !isEmptyDataObject(thisCache) : !jQuery.isEmptyObject(thisCache) ) { - return; + delete cache[ name[ i ] ]; } } + + // Remove the expando if there's no more data + if ( key === undefined || jQuery.isEmptyObject( cache ) ) { + + // Support: Chrome <= 35-45+ + // Webkit & Blink performance suffers when deleting properties + // from DOM nodes, so set to undefined instead + // https://code.google.com/p/chromium/issues/detail?id=378607 + if ( owner.nodeType ) { + owner[ this.expando ] = undefined; + } else { + delete owner[ this.expando ]; + } + } + }, + hasData: function( owner ) { + var cache = owner[ this.expando ]; + return cache !== undefined && !jQuery.isEmptyObject( cache ); } +}; +var dataPriv = new Data(); - // See jQuery.data for more information - if ( !pvt ) { - delete cache[ id ].data; +var dataUser = new Data(); - // Don't destroy the parent cache unless the internal data object - // had been the only thing left in it - if ( !isEmptyDataObject( cache[ id ] ) ) { - return; + + +// Implementation Summary +// +// 1. Enforce API surface and semantic compatibility with 1.9.x branch +// 2. Improve the module's maintainability by reducing the storage +// paths to a single mechanism. +// 3. Use the same single mechanism to support "private" and "user" data. +// 4. _Never_ expose "private" data to user code (TODO: Drop _data, _removeData) +// 5. Avoid exposing implementation details on user objects (eg. expando properties) +// 6. Provide a clear path for implementation upgrade to WeakMap in 2014 + +var rbrace = /^(?:\{[\w\W]*\}|\[[\w\W]*\])$/, + rmultiDash = /[A-Z]/g; + +function dataAttr( elem, key, data ) { + var name; + + // If nothing was found internally, try to fetch any + // data from the HTML5 data-* attribute + if ( data === undefined && elem.nodeType === 1 ) { + name = "data-" + key.replace( rmultiDash, "-$&" ).toLowerCase(); + data = elem.getAttribute( name ); + + if ( typeof data === "string" ) { + try { + data = data === "true" ? true : + data === "false" ? false : + data === "null" ? null : + + // Only convert to a number if it doesn't change the string + +data + "" === data ? +data : + rbrace.test( data ) ? jQuery.parseJSON( data ) : + data; + } catch ( e ) {} + + // Make sure we set the data so it isn't changed later + dataUser.set( elem, key, data ); + } else { + data = undefined; } } - - // Destroy the cache - if ( isNode ) { - jQuery.cleanData( [ elem ], true ); - - // Use delete when supported for expandos or `cache` is not a window per isWindow (#10080) - /* jshint eqeqeq: false */ - } else if ( jQuery.support.deleteExpando || cache != cache.window ) { - /* jshint eqeqeq: true */ - delete cache[ id ]; - - // When all else fails, null - } else { - cache[ id ] = null; - } + return data; } -jQuery.extend({ - cache: {}, - - // The following elements throw uncatchable exceptions if you - // attempt to add expando properties to them. - noData: { - "applet": true, - "embed": true, - // Ban all objects except for Flash (which handle expandos) - "object": "clsid:D27CDB6E-AE6D-11cf-96B8-444553540000" - }, - +jQuery.extend( { hasData: function( elem ) { - elem = elem.nodeType ? jQuery.cache[ elem[jQuery.expando] ] : elem[ jQuery.expando ]; - return !!elem && !isEmptyDataObject( elem ); + return dataUser.hasData( elem ) || dataPriv.hasData( elem ); }, data: function( elem, name, data ) { - return internalData( elem, name, data ); + return dataUser.access( elem, name, data ); }, removeData: function( elem, name ) { - return internalRemoveData( elem, name ); + dataUser.remove( elem, name ); }, - // For internal use only. + // TODO: Now that all calls to _data and _removeData have been replaced + // with direct calls to dataPriv methods, these can be deprecated. _data: function( elem, name, data ) { - return internalData( elem, name, data, true ); + return dataPriv.access( elem, name, data ); }, _removeData: function( elem, name ) { - return internalRemoveData( elem, name, true ); - }, - - // A method for determining if a DOM node can handle the data expando - acceptData: function( elem ) { - // Do not set data on non-element because it will not be cleared (#8335). - if ( elem.nodeType && elem.nodeType !== 1 && elem.nodeType !== 9 ) { - return false; - } - - var noData = elem.nodeName && jQuery.noData[ elem.nodeName.toLowerCase() ]; - - // nodes accept data unless otherwise specified; rejection can be conditional - return !noData || noData !== true && elem.getAttribute("classid") === noData; + dataPriv.remove( elem, name ); } -}); +} ); -jQuery.fn.extend({ +jQuery.fn.extend( { data: function( key, value ) { - var attrs, name, - data = null, - i = 0, - elem = this[0]; - - // Special expections of .data basically thwart jQuery.access, - // so implement the relevant behavior ourselves + var i, name, data, + elem = this[ 0 ], + attrs = elem && elem.attributes; // Gets all values if ( key === undefined ) { if ( this.length ) { - data = jQuery.data( elem ); + data = dataUser.get( elem ); - if ( elem.nodeType === 1 && !jQuery._data( elem, "parsedAttrs" ) ) { - attrs = elem.attributes; - for ( ; i < attrs.length; i++ ) { - name = attrs[i].name; + if ( elem.nodeType === 1 && !dataPriv.get( elem, "hasDataAttrs" ) ) { + i = attrs.length; + while ( i-- ) { - if ( name.indexOf("data-") === 0 ) { - name = jQuery.camelCase( name.slice(5) ); - - dataAttr( elem, name, data[ name ] ); + // Support: IE11+ + // The attrs elements can be null (#14894) + if ( attrs[ i ] ) { + name = attrs[ i ].name; + if ( name.indexOf( "data-" ) === 0 ) { + name = jQuery.camelCase( name.slice( 5 ) ); + dataAttr( elem, name, data[ name ] ); + } } } - jQuery._data( elem, "parsedAttrs", true ); + dataPriv.set( elem, "hasDataAttrs", true ); } } @@ -3828,89 +3956,96 @@ jQuery.fn.extend({ // Sets multiple values if ( typeof key === "object" ) { - return this.each(function() { - jQuery.data( this, key ); - }); + return this.each( function() { + dataUser.set( this, key ); + } ); } - return arguments.length > 1 ? + return access( this, function( value ) { + var data, camelKey; - // Sets one value - this.each(function() { - jQuery.data( this, key, value ); - }) : + // The calling jQuery object (element matches) is not empty + // (and therefore has an element appears at this[ 0 ]) and the + // `value` parameter was not undefined. An empty jQuery object + // will result in `undefined` for elem = this[ 0 ] which will + // throw an exception if an attempt to read a data cache is made. + if ( elem && value === undefined ) { - // Gets one value - // Try to fetch any internally stored data first - elem ? dataAttr( elem, key, jQuery.data( elem, key ) ) : null; + // Attempt to get data from the cache + // with the key as-is + data = dataUser.get( elem, key ) || + + // Try to find dashed key if it exists (gh-2779) + // This is for 2.2.x only + dataUser.get( elem, key.replace( rmultiDash, "-$&" ).toLowerCase() ); + + if ( data !== undefined ) { + return data; + } + + camelKey = jQuery.camelCase( key ); + + // Attempt to get data from the cache + // with the key camelized + data = dataUser.get( elem, camelKey ); + if ( data !== undefined ) { + return data; + } + + // Attempt to "discover" the data in + // HTML5 custom data-* attrs + data = dataAttr( elem, camelKey, undefined ); + if ( data !== undefined ) { + return data; + } + + // We tried really hard, but the data doesn't exist. + return; + } + + // Set the data... + camelKey = jQuery.camelCase( key ); + this.each( function() { + + // First, attempt to store a copy or reference of any + // data that might've been store with a camelCased key. + var data = dataUser.get( this, camelKey ); + + // For HTML5 data-* attribute interop, we have to + // store property names with dashes in a camelCase form. + // This might not apply to all properties...* + dataUser.set( this, camelKey, value ); + + // *... In the case of properties that might _actually_ + // have dashes, we need to also store a copy of that + // unchanged property. + if ( key.indexOf( "-" ) > -1 && data !== undefined ) { + dataUser.set( this, key, value ); + } + } ); + }, null, value, arguments.length > 1, null, true ); }, removeData: function( key ) { - return this.each(function() { - jQuery.removeData( this, key ); - }); + return this.each( function() { + dataUser.remove( this, key ); + } ); } -}); +} ); -function dataAttr( elem, key, data ) { - // If nothing was found internally, try to fetch any - // data from the HTML5 data-* attribute - if ( data === undefined && elem.nodeType === 1 ) { - var name = "data-" + key.replace( rmultiDash, "-$1" ).toLowerCase(); - - data = elem.getAttribute( name ); - - if ( typeof data === "string" ) { - try { - data = data === "true" ? true : - data === "false" ? false : - data === "null" ? null : - // Only convert to a number if it doesn't change the string - +data + "" === data ? +data : - rbrace.test( data ) ? jQuery.parseJSON( data ) : - data; - } catch( e ) {} - - // Make sure we set the data so it isn't changed later - jQuery.data( elem, key, data ); - - } else { - data = undefined; - } - } - - return data; -} - -// checks a cache object for emptiness -function isEmptyDataObject( obj ) { - var name; - for ( name in obj ) { - - // if the public data object is empty, the private is still empty - if ( name === "data" && jQuery.isEmptyObject( obj[name] ) ) { - continue; - } - if ( name !== "toJSON" ) { - return false; - } - } - - return true; -} -jQuery.extend({ +jQuery.extend( { queue: function( elem, type, data ) { var queue; if ( elem ) { type = ( type || "fx" ) + "queue"; - queue = jQuery._data( elem, type ); + queue = dataPriv.get( elem, type ); // Speed up dequeue by getting out quickly if this is just a lookup if ( data ) { - if ( !queue || jQuery.isArray(data) ) { - queue = jQuery._data( elem, type, jQuery.makeArray(data) ); + if ( !queue || jQuery.isArray( data ) ) { + queue = dataPriv.access( elem, type, jQuery.makeArray( data ) ); } else { queue.push( data ); } @@ -3944,7 +4079,7 @@ jQuery.extend({ queue.unshift( "inprogress" ); } - // clear up the last queue stop function + // Clear up the last queue stop function delete hooks.stop; fn.call( elem, next, hooks ); } @@ -3954,19 +4089,18 @@ jQuery.extend({ } }, - // not intended for public consumption - generates a queueHooks object, or returns the current one + // Not public - generate a queueHooks object, or return the current one _queueHooks: function( elem, type ) { var key = type + "queueHooks"; - return jQuery._data( elem, key ) || jQuery._data( elem, key, { - empty: jQuery.Callbacks("once memory").add(function() { - jQuery._removeData( elem, type + "queue" ); - jQuery._removeData( elem, key ); - }) - }); + return dataPriv.get( elem, key ) || dataPriv.access( elem, key, { + empty: jQuery.Callbacks( "once memory" ).add( function() { + dataPriv.remove( elem, [ type + "queue", key ] ); + } ) + } ); } -}); +} ); -jQuery.fn.extend({ +jQuery.fn.extend( { queue: function( type, data ) { var setter = 2; @@ -3977,43 +4111,31 @@ jQuery.fn.extend({ } if ( arguments.length < setter ) { - return jQuery.queue( this[0], type ); + return jQuery.queue( this[ 0 ], type ); } return data === undefined ? this : - this.each(function() { + this.each( function() { var queue = jQuery.queue( this, type, data ); - // ensure a hooks for this queue + // Ensure a hooks for this queue jQuery._queueHooks( this, type ); - if ( type === "fx" && queue[0] !== "inprogress" ) { + if ( type === "fx" && queue[ 0 ] !== "inprogress" ) { jQuery.dequeue( this, type ); } - }); + } ); }, dequeue: function( type ) { - return this.each(function() { + return this.each( function() { jQuery.dequeue( this, type ); - }); - }, - // Based off of the plugin by Clint Helfers, with permission. - // http://blindsignals.com/index.php/2009/07/jquery-delay/ - delay: function( time, type ) { - time = jQuery.fx ? jQuery.fx.speeds[ time ] || time : time; - type = type || "fx"; - - return this.queue( type, function( next, hooks ) { - var timeout = setTimeout( next, time ); - hooks.stop = function() { - clearTimeout( timeout ); - }; - }); + } ); }, clearQueue: function( type ) { return this.queue( type || "fx", [] ); }, + // Get a promise resolved when queues of a certain type // are emptied (fx is the type by default) promise: function( type, obj ) { @@ -4034,8 +4156,8 @@ jQuery.fn.extend({ } type = type || "fx"; - while( i-- ) { - tmp = jQuery._data( elements[ i ], type + "queueHooks" ); + while ( i-- ) { + tmp = dataPriv.get( elements[ i ], type + "queueHooks" ); if ( tmp && tmp.empty ) { count++; tmp.empty.add( resolve ); @@ -4044,74 +4166,3255 @@ jQuery.fn.extend({ resolve(); return defer.promise( obj ); } -}); -var nodeHook, boolHook, - rclass = /[\t\r\n\f]/g, - rreturn = /\r/g, - rfocusable = /^(?:input|select|textarea|button|object)$/i, - rclickable = /^(?:a|area)$/i, - ruseDefault = /^(?:checked|selected)$/i, - getSetAttribute = jQuery.support.getSetAttribute, - getSetInput = jQuery.support.input; +} ); +var pnum = ( /[+-]?(?:\d*\.|)\d+(?:[eE][+-]?\d+|)/ ).source; -jQuery.fn.extend({ +var rcssNum = new RegExp( "^(?:([+-])=|)(" + pnum + ")([a-z%]*)$", "i" ); + + +var cssExpand = [ "Top", "Right", "Bottom", "Left" ]; + +var isHidden = function( elem, el ) { + + // isHidden might be called from jQuery#filter function; + // in that case, element will be second argument + elem = el || elem; + return jQuery.css( elem, "display" ) === "none" || + !jQuery.contains( elem.ownerDocument, elem ); + }; + + + +function adjustCSS( elem, prop, valueParts, tween ) { + var adjusted, + scale = 1, + maxIterations = 20, + currentValue = tween ? + function() { return tween.cur(); } : + function() { return jQuery.css( elem, prop, "" ); }, + initial = currentValue(), + unit = valueParts && valueParts[ 3 ] || ( jQuery.cssNumber[ prop ] ? "" : "px" ), + + // Starting value computation is required for potential unit mismatches + initialInUnit = ( jQuery.cssNumber[ prop ] || unit !== "px" && +initial ) && + rcssNum.exec( jQuery.css( elem, prop ) ); + + if ( initialInUnit && initialInUnit[ 3 ] !== unit ) { + + // Trust units reported by jQuery.css + unit = unit || initialInUnit[ 3 ]; + + // Make sure we update the tween properties later on + valueParts = valueParts || []; + + // Iteratively approximate from a nonzero starting point + initialInUnit = +initial || 1; + + do { + + // If previous iteration zeroed out, double until we get *something*. + // Use string for doubling so we don't accidentally see scale as unchanged below + scale = scale || ".5"; + + // Adjust and apply + initialInUnit = initialInUnit / scale; + jQuery.style( elem, prop, initialInUnit + unit ); + + // Update scale, tolerating zero or NaN from tween.cur() + // Break the loop if scale is unchanged or perfect, or if we've just had enough. + } while ( + scale !== ( scale = currentValue() / initial ) && scale !== 1 && --maxIterations + ); + } + + if ( valueParts ) { + initialInUnit = +initialInUnit || +initial || 0; + + // Apply relative offset (+=/-=) if specified + adjusted = valueParts[ 1 ] ? + initialInUnit + ( valueParts[ 1 ] + 1 ) * valueParts[ 2 ] : + +valueParts[ 2 ]; + if ( tween ) { + tween.unit = unit; + tween.start = initialInUnit; + tween.end = adjusted; + } + } + return adjusted; +} +var rcheckableType = ( /^(?:checkbox|radio)$/i ); + +var rtagName = ( /<([\w:-]+)/ ); + +var rscriptType = ( /^$|\/(?:java|ecma)script/i ); + + + +// We have to close these tags to support XHTML (#13200) +var wrapMap = { + + // Support: IE9 + option: [ 1, "" ], + + // XHTML parsers do not magically insert elements in the + // same way that tag soup parsers do. So we cannot shorten + // this by omitting or other required elements. + thead: [ 1, "", "
        " ], + col: [ 2, "", "
        " ], + tr: [ 2, "", "
        " ], + td: [ 3, "", "
        " ], + + _default: [ 0, "", "" ] +}; + +// Support: IE9 +wrapMap.optgroup = wrapMap.option; + +wrapMap.tbody = wrapMap.tfoot = wrapMap.colgroup = wrapMap.caption = wrapMap.thead; +wrapMap.th = wrapMap.td; + + +function getAll( context, tag ) { + + // Support: IE9-11+ + // Use typeof to avoid zero-argument method invocation on host objects (#15151) + var ret = typeof context.getElementsByTagName !== "undefined" ? + context.getElementsByTagName( tag || "*" ) : + typeof context.querySelectorAll !== "undefined" ? + context.querySelectorAll( tag || "*" ) : + []; + + return tag === undefined || tag && jQuery.nodeName( context, tag ) ? + jQuery.merge( [ context ], ret ) : + ret; +} + + +// Mark scripts as having already been evaluated +function setGlobalEval( elems, refElements ) { + var i = 0, + l = elems.length; + + for ( ; i < l; i++ ) { + dataPriv.set( + elems[ i ], + "globalEval", + !refElements || dataPriv.get( refElements[ i ], "globalEval" ) + ); + } +} + + +var rhtml = /<|&#?\w+;/; + +function buildFragment( elems, context, scripts, selection, ignored ) { + var elem, tmp, tag, wrap, contains, j, + fragment = context.createDocumentFragment(), + nodes = [], + i = 0, + l = elems.length; + + for ( ; i < l; i++ ) { + elem = elems[ i ]; + + if ( elem || elem === 0 ) { + + // Add nodes directly + if ( jQuery.type( elem ) === "object" ) { + + // Support: Android<4.1, PhantomJS<2 + // push.apply(_, arraylike) throws on ancient WebKit + jQuery.merge( nodes, elem.nodeType ? [ elem ] : elem ); + + // Convert non-html into a text node + } else if ( !rhtml.test( elem ) ) { + nodes.push( context.createTextNode( elem ) ); + + // Convert html into DOM nodes + } else { + tmp = tmp || fragment.appendChild( context.createElement( "div" ) ); + + // Deserialize a standard representation + tag = ( rtagName.exec( elem ) || [ "", "" ] )[ 1 ].toLowerCase(); + wrap = wrapMap[ tag ] || wrapMap._default; + tmp.innerHTML = wrap[ 1 ] + jQuery.htmlPrefilter( elem ) + wrap[ 2 ]; + + // Descend through wrappers to the right content + j = wrap[ 0 ]; + while ( j-- ) { + tmp = tmp.lastChild; + } + + // Support: Android<4.1, PhantomJS<2 + // push.apply(_, arraylike) throws on ancient WebKit + jQuery.merge( nodes, tmp.childNodes ); + + // Remember the top-level container + tmp = fragment.firstChild; + + // Ensure the created nodes are orphaned (#12392) + tmp.textContent = ""; + } + } + } + + // Remove wrapper from fragment + fragment.textContent = ""; + + i = 0; + while ( ( elem = nodes[ i++ ] ) ) { + + // Skip elements already in the context collection (trac-4087) + if ( selection && jQuery.inArray( elem, selection ) > -1 ) { + if ( ignored ) { + ignored.push( elem ); + } + continue; + } + + contains = jQuery.contains( elem.ownerDocument, elem ); + + // Append to fragment + tmp = getAll( fragment.appendChild( elem ), "script" ); + + // Preserve script evaluation history + if ( contains ) { + setGlobalEval( tmp ); + } + + // Capture executables + if ( scripts ) { + j = 0; + while ( ( elem = tmp[ j++ ] ) ) { + if ( rscriptType.test( elem.type || "" ) ) { + scripts.push( elem ); + } + } + } + } + + return fragment; +} + + +( function() { + var fragment = document.createDocumentFragment(), + div = fragment.appendChild( document.createElement( "div" ) ), + input = document.createElement( "input" ); + + // Support: Android 4.0-4.3, Safari<=5.1 + // Check state lost if the name is set (#11217) + // Support: Windows Web Apps (WWA) + // `name` and `type` must use .setAttribute for WWA (#14901) + input.setAttribute( "type", "radio" ); + input.setAttribute( "checked", "checked" ); + input.setAttribute( "name", "t" ); + + div.appendChild( input ); + + // Support: Safari<=5.1, Android<4.2 + // Older WebKit doesn't clone checked state correctly in fragments + support.checkClone = div.cloneNode( true ).cloneNode( true ).lastChild.checked; + + // Support: IE<=11+ + // Make sure textarea (and checkbox) defaultValue is properly cloned + div.innerHTML = ""; + support.noCloneChecked = !!div.cloneNode( true ).lastChild.defaultValue; +} )(); + + +var + rkeyEvent = /^key/, + rmouseEvent = /^(?:mouse|pointer|contextmenu|drag|drop)|click/, + rtypenamespace = /^([^.]*)(?:\.(.+)|)/; + +function returnTrue() { + return true; +} + +function returnFalse() { + return false; +} + +// Support: IE9 +// See #13393 for more info +function safeActiveElement() { + try { + return document.activeElement; + } catch ( err ) { } +} + +function on( elem, types, selector, data, fn, one ) { + var origFn, type; + + // Types can be a map of types/handlers + if ( typeof types === "object" ) { + + // ( types-Object, selector, data ) + if ( typeof selector !== "string" ) { + + // ( types-Object, data ) + data = data || selector; + selector = undefined; + } + for ( type in types ) { + on( elem, type, selector, data, types[ type ], one ); + } + return elem; + } + + if ( data == null && fn == null ) { + + // ( types, fn ) + fn = selector; + data = selector = undefined; + } else if ( fn == null ) { + if ( typeof selector === "string" ) { + + // ( types, selector, fn ) + fn = data; + data = undefined; + } else { + + // ( types, data, fn ) + fn = data; + data = selector; + selector = undefined; + } + } + if ( fn === false ) { + fn = returnFalse; + } else if ( !fn ) { + return elem; + } + + if ( one === 1 ) { + origFn = fn; + fn = function( event ) { + + // Can use an empty set, since event contains the info + jQuery().off( event ); + return origFn.apply( this, arguments ); + }; + + // Use same guid so caller can remove using origFn + fn.guid = origFn.guid || ( origFn.guid = jQuery.guid++ ); + } + return elem.each( function() { + jQuery.event.add( this, types, fn, data, selector ); + } ); +} + +/* + * Helper functions for managing events -- not part of the public interface. + * Props to Dean Edwards' addEvent library for many of the ideas. + */ +jQuery.event = { + + global: {}, + + add: function( elem, types, handler, data, selector ) { + + var handleObjIn, eventHandle, tmp, + events, t, handleObj, + special, handlers, type, namespaces, origType, + elemData = dataPriv.get( elem ); + + // Don't attach events to noData or text/comment nodes (but allow plain objects) + if ( !elemData ) { + return; + } + + // Caller can pass in an object of custom data in lieu of the handler + if ( handler.handler ) { + handleObjIn = handler; + handler = handleObjIn.handler; + selector = handleObjIn.selector; + } + + // Make sure that the handler has a unique ID, used to find/remove it later + if ( !handler.guid ) { + handler.guid = jQuery.guid++; + } + + // Init the element's event structure and main handler, if this is the first + if ( !( events = elemData.events ) ) { + events = elemData.events = {}; + } + if ( !( eventHandle = elemData.handle ) ) { + eventHandle = elemData.handle = function( e ) { + + // Discard the second event of a jQuery.event.trigger() and + // when an event is called after a page has unloaded + return typeof jQuery !== "undefined" && jQuery.event.triggered !== e.type ? + jQuery.event.dispatch.apply( elem, arguments ) : undefined; + }; + } + + // Handle multiple events separated by a space + types = ( types || "" ).match( rnotwhite ) || [ "" ]; + t = types.length; + while ( t-- ) { + tmp = rtypenamespace.exec( types[ t ] ) || []; + type = origType = tmp[ 1 ]; + namespaces = ( tmp[ 2 ] || "" ).split( "." ).sort(); + + // There *must* be a type, no attaching namespace-only handlers + if ( !type ) { + continue; + } + + // If event changes its type, use the special event handlers for the changed type + special = jQuery.event.special[ type ] || {}; + + // If selector defined, determine special event api type, otherwise given type + type = ( selector ? special.delegateType : special.bindType ) || type; + + // Update special based on newly reset type + special = jQuery.event.special[ type ] || {}; + + // handleObj is passed to all event handlers + handleObj = jQuery.extend( { + type: type, + origType: origType, + data: data, + handler: handler, + guid: handler.guid, + selector: selector, + needsContext: selector && jQuery.expr.match.needsContext.test( selector ), + namespace: namespaces.join( "." ) + }, handleObjIn ); + + // Init the event handler queue if we're the first + if ( !( handlers = events[ type ] ) ) { + handlers = events[ type ] = []; + handlers.delegateCount = 0; + + // Only use addEventListener if the special events handler returns false + if ( !special.setup || + special.setup.call( elem, data, namespaces, eventHandle ) === false ) { + + if ( elem.addEventListener ) { + elem.addEventListener( type, eventHandle ); + } + } + } + + if ( special.add ) { + special.add.call( elem, handleObj ); + + if ( !handleObj.handler.guid ) { + handleObj.handler.guid = handler.guid; + } + } + + // Add to the element's handler list, delegates in front + if ( selector ) { + handlers.splice( handlers.delegateCount++, 0, handleObj ); + } else { + handlers.push( handleObj ); + } + + // Keep track of which events have ever been used, for event optimization + jQuery.event.global[ type ] = true; + } + + }, + + // Detach an event or set of events from an element + remove: function( elem, types, handler, selector, mappedTypes ) { + + var j, origCount, tmp, + events, t, handleObj, + special, handlers, type, namespaces, origType, + elemData = dataPriv.hasData( elem ) && dataPriv.get( elem ); + + if ( !elemData || !( events = elemData.events ) ) { + return; + } + + // Once for each type.namespace in types; type may be omitted + types = ( types || "" ).match( rnotwhite ) || [ "" ]; + t = types.length; + while ( t-- ) { + tmp = rtypenamespace.exec( types[ t ] ) || []; + type = origType = tmp[ 1 ]; + namespaces = ( tmp[ 2 ] || "" ).split( "." ).sort(); + + // Unbind all events (on this namespace, if provided) for the element + if ( !type ) { + for ( type in events ) { + jQuery.event.remove( elem, type + types[ t ], handler, selector, true ); + } + continue; + } + + special = jQuery.event.special[ type ] || {}; + type = ( selector ? special.delegateType : special.bindType ) || type; + handlers = events[ type ] || []; + tmp = tmp[ 2 ] && + new RegExp( "(^|\\.)" + namespaces.join( "\\.(?:.*\\.|)" ) + "(\\.|$)" ); + + // Remove matching events + origCount = j = handlers.length; + while ( j-- ) { + handleObj = handlers[ j ]; + + if ( ( mappedTypes || origType === handleObj.origType ) && + ( !handler || handler.guid === handleObj.guid ) && + ( !tmp || tmp.test( handleObj.namespace ) ) && + ( !selector || selector === handleObj.selector || + selector === "**" && handleObj.selector ) ) { + handlers.splice( j, 1 ); + + if ( handleObj.selector ) { + handlers.delegateCount--; + } + if ( special.remove ) { + special.remove.call( elem, handleObj ); + } + } + } + + // Remove generic event handler if we removed something and no more handlers exist + // (avoids potential for endless recursion during removal of special event handlers) + if ( origCount && !handlers.length ) { + if ( !special.teardown || + special.teardown.call( elem, namespaces, elemData.handle ) === false ) { + + jQuery.removeEvent( elem, type, elemData.handle ); + } + + delete events[ type ]; + } + } + + // Remove data and the expando if it's no longer used + if ( jQuery.isEmptyObject( events ) ) { + dataPriv.remove( elem, "handle events" ); + } + }, + + dispatch: function( event ) { + + // Make a writable jQuery.Event from the native event object + event = jQuery.event.fix( event ); + + var i, j, ret, matched, handleObj, + handlerQueue = [], + args = slice.call( arguments ), + handlers = ( dataPriv.get( this, "events" ) || {} )[ event.type ] || [], + special = jQuery.event.special[ event.type ] || {}; + + // Use the fix-ed jQuery.Event rather than the (read-only) native event + args[ 0 ] = event; + event.delegateTarget = this; + + // Call the preDispatch hook for the mapped type, and let it bail if desired + if ( special.preDispatch && special.preDispatch.call( this, event ) === false ) { + return; + } + + // Determine handlers + handlerQueue = jQuery.event.handlers.call( this, event, handlers ); + + // Run delegates first; they may want to stop propagation beneath us + i = 0; + while ( ( matched = handlerQueue[ i++ ] ) && !event.isPropagationStopped() ) { + event.currentTarget = matched.elem; + + j = 0; + while ( ( handleObj = matched.handlers[ j++ ] ) && + !event.isImmediatePropagationStopped() ) { + + // Triggered event must either 1) have no namespace, or 2) have namespace(s) + // a subset or equal to those in the bound event (both can have no namespace). + if ( !event.rnamespace || event.rnamespace.test( handleObj.namespace ) ) { + + event.handleObj = handleObj; + event.data = handleObj.data; + + ret = ( ( jQuery.event.special[ handleObj.origType ] || {} ).handle || + handleObj.handler ).apply( matched.elem, args ); + + if ( ret !== undefined ) { + if ( ( event.result = ret ) === false ) { + event.preventDefault(); + event.stopPropagation(); + } + } + } + } + } + + // Call the postDispatch hook for the mapped type + if ( special.postDispatch ) { + special.postDispatch.call( this, event ); + } + + return event.result; + }, + + handlers: function( event, handlers ) { + var i, matches, sel, handleObj, + handlerQueue = [], + delegateCount = handlers.delegateCount, + cur = event.target; + + // Support (at least): Chrome, IE9 + // Find delegate handlers + // Black-hole SVG instance trees (#13180) + // + // Support: Firefox<=42+ + // Avoid non-left-click in FF but don't block IE radio events (#3861, gh-2343) + if ( delegateCount && cur.nodeType && + ( event.type !== "click" || isNaN( event.button ) || event.button < 1 ) ) { + + for ( ; cur !== this; cur = cur.parentNode || this ) { + + // Don't check non-elements (#13208) + // Don't process clicks on disabled elements (#6911, #8165, #11382, #11764) + if ( cur.nodeType === 1 && ( cur.disabled !== true || event.type !== "click" ) ) { + matches = []; + for ( i = 0; i < delegateCount; i++ ) { + handleObj = handlers[ i ]; + + // Don't conflict with Object.prototype properties (#13203) + sel = handleObj.selector + " "; + + if ( matches[ sel ] === undefined ) { + matches[ sel ] = handleObj.needsContext ? + jQuery( sel, this ).index( cur ) > -1 : + jQuery.find( sel, this, null, [ cur ] ).length; + } + if ( matches[ sel ] ) { + matches.push( handleObj ); + } + } + if ( matches.length ) { + handlerQueue.push( { elem: cur, handlers: matches } ); + } + } + } + } + + // Add the remaining (directly-bound) handlers + if ( delegateCount < handlers.length ) { + handlerQueue.push( { elem: this, handlers: handlers.slice( delegateCount ) } ); + } + + return handlerQueue; + }, + + // Includes some event props shared by KeyEvent and MouseEvent + props: ( "altKey bubbles cancelable ctrlKey currentTarget detail eventPhase " + + "metaKey relatedTarget shiftKey target timeStamp view which" ).split( " " ), + + fixHooks: {}, + + keyHooks: { + props: "char charCode key keyCode".split( " " ), + filter: function( event, original ) { + + // Add which for key events + if ( event.which == null ) { + event.which = original.charCode != null ? original.charCode : original.keyCode; + } + + return event; + } + }, + + mouseHooks: { + props: ( "button buttons clientX clientY offsetX offsetY pageX pageY " + + "screenX screenY toElement" ).split( " " ), + filter: function( event, original ) { + var eventDoc, doc, body, + button = original.button; + + // Calculate pageX/Y if missing and clientX/Y available + if ( event.pageX == null && original.clientX != null ) { + eventDoc = event.target.ownerDocument || document; + doc = eventDoc.documentElement; + body = eventDoc.body; + + event.pageX = original.clientX + + ( doc && doc.scrollLeft || body && body.scrollLeft || 0 ) - + ( doc && doc.clientLeft || body && body.clientLeft || 0 ); + event.pageY = original.clientY + + ( doc && doc.scrollTop || body && body.scrollTop || 0 ) - + ( doc && doc.clientTop || body && body.clientTop || 0 ); + } + + // Add which for click: 1 === left; 2 === middle; 3 === right + // Note: button is not normalized, so don't use it + if ( !event.which && button !== undefined ) { + event.which = ( button & 1 ? 1 : ( button & 2 ? 3 : ( button & 4 ? 2 : 0 ) ) ); + } + + return event; + } + }, + + fix: function( event ) { + if ( event[ jQuery.expando ] ) { + return event; + } + + // Create a writable copy of the event object and normalize some properties + var i, prop, copy, + type = event.type, + originalEvent = event, + fixHook = this.fixHooks[ type ]; + + if ( !fixHook ) { + this.fixHooks[ type ] = fixHook = + rmouseEvent.test( type ) ? this.mouseHooks : + rkeyEvent.test( type ) ? this.keyHooks : + {}; + } + copy = fixHook.props ? this.props.concat( fixHook.props ) : this.props; + + event = new jQuery.Event( originalEvent ); + + i = copy.length; + while ( i-- ) { + prop = copy[ i ]; + event[ prop ] = originalEvent[ prop ]; + } + + // Support: Cordova 2.5 (WebKit) (#13255) + // All events should have a target; Cordova deviceready doesn't + if ( !event.target ) { + event.target = document; + } + + // Support: Safari 6.0+, Chrome<28 + // Target should not be a text node (#504, #13143) + if ( event.target.nodeType === 3 ) { + event.target = event.target.parentNode; + } + + return fixHook.filter ? fixHook.filter( event, originalEvent ) : event; + }, + + special: { + load: { + + // Prevent triggered image.load events from bubbling to window.load + noBubble: true + }, + focus: { + + // Fire native event if possible so blur/focus sequence is correct + trigger: function() { + if ( this !== safeActiveElement() && this.focus ) { + this.focus(); + return false; + } + }, + delegateType: "focusin" + }, + blur: { + trigger: function() { + if ( this === safeActiveElement() && this.blur ) { + this.blur(); + return false; + } + }, + delegateType: "focusout" + }, + click: { + + // For checkbox, fire native event so checked state will be right + trigger: function() { + if ( this.type === "checkbox" && this.click && jQuery.nodeName( this, "input" ) ) { + this.click(); + return false; + } + }, + + // For cross-browser consistency, don't fire native .click() on links + _default: function( event ) { + return jQuery.nodeName( event.target, "a" ); + } + }, + + beforeunload: { + postDispatch: function( event ) { + + // Support: Firefox 20+ + // Firefox doesn't alert if the returnValue field is not set. + if ( event.result !== undefined && event.originalEvent ) { + event.originalEvent.returnValue = event.result; + } + } + } + } +}; + +jQuery.removeEvent = function( elem, type, handle ) { + + // This "if" is needed for plain objects + if ( elem.removeEventListener ) { + elem.removeEventListener( type, handle ); + } +}; + +jQuery.Event = function( src, props ) { + + // Allow instantiation without the 'new' keyword + if ( !( this instanceof jQuery.Event ) ) { + return new jQuery.Event( src, props ); + } + + // Event object + if ( src && src.type ) { + this.originalEvent = src; + this.type = src.type; + + // Events bubbling up the document may have been marked as prevented + // by a handler lower down the tree; reflect the correct value. + this.isDefaultPrevented = src.defaultPrevented || + src.defaultPrevented === undefined && + + // Support: Android<4.0 + src.returnValue === false ? + returnTrue : + returnFalse; + + // Event type + } else { + this.type = src; + } + + // Put explicitly provided properties onto the event object + if ( props ) { + jQuery.extend( this, props ); + } + + // Create a timestamp if incoming event doesn't have one + this.timeStamp = src && src.timeStamp || jQuery.now(); + + // Mark it as fixed + this[ jQuery.expando ] = true; +}; + +// jQuery.Event is based on DOM3 Events as specified by the ECMAScript Language Binding +// http://www.w3.org/TR/2003/WD-DOM-Level-3-Events-20030331/ecma-script-binding.html +jQuery.Event.prototype = { + constructor: jQuery.Event, + isDefaultPrevented: returnFalse, + isPropagationStopped: returnFalse, + isImmediatePropagationStopped: returnFalse, + + preventDefault: function() { + var e = this.originalEvent; + + this.isDefaultPrevented = returnTrue; + + if ( e ) { + e.preventDefault(); + } + }, + stopPropagation: function() { + var e = this.originalEvent; + + this.isPropagationStopped = returnTrue; + + if ( e ) { + e.stopPropagation(); + } + }, + stopImmediatePropagation: function() { + var e = this.originalEvent; + + this.isImmediatePropagationStopped = returnTrue; + + if ( e ) { + e.stopImmediatePropagation(); + } + + this.stopPropagation(); + } +}; + +// Create mouseenter/leave events using mouseover/out and event-time checks +// so that event delegation works in jQuery. +// Do the same for pointerenter/pointerleave and pointerover/pointerout +// +// Support: Safari 7 only +// Safari sends mouseenter too often; see: +// https://code.google.com/p/chromium/issues/detail?id=470258 +// for the description of the bug (it existed in older Chrome versions as well). +jQuery.each( { + mouseenter: "mouseover", + mouseleave: "mouseout", + pointerenter: "pointerover", + pointerleave: "pointerout" +}, function( orig, fix ) { + jQuery.event.special[ orig ] = { + delegateType: fix, + bindType: fix, + + handle: function( event ) { + var ret, + target = this, + related = event.relatedTarget, + handleObj = event.handleObj; + + // For mouseenter/leave call the handler if related is outside the target. + // NB: No relatedTarget if the mouse left/entered the browser window + if ( !related || ( related !== target && !jQuery.contains( target, related ) ) ) { + event.type = handleObj.origType; + ret = handleObj.handler.apply( this, arguments ); + event.type = fix; + } + return ret; + } + }; +} ); + +jQuery.fn.extend( { + on: function( types, selector, data, fn ) { + return on( this, types, selector, data, fn ); + }, + one: function( types, selector, data, fn ) { + return on( this, types, selector, data, fn, 1 ); + }, + off: function( types, selector, fn ) { + var handleObj, type; + if ( types && types.preventDefault && types.handleObj ) { + + // ( event ) dispatched jQuery.Event + handleObj = types.handleObj; + jQuery( types.delegateTarget ).off( + handleObj.namespace ? + handleObj.origType + "." + handleObj.namespace : + handleObj.origType, + handleObj.selector, + handleObj.handler + ); + return this; + } + if ( typeof types === "object" ) { + + // ( types-object [, selector] ) + for ( type in types ) { + this.off( type, selector, types[ type ] ); + } + return this; + } + if ( selector === false || typeof selector === "function" ) { + + // ( types [, fn] ) + fn = selector; + selector = undefined; + } + if ( fn === false ) { + fn = returnFalse; + } + return this.each( function() { + jQuery.event.remove( this, types, fn, selector ); + } ); + } +} ); + + +var + rxhtmlTag = /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:-]+)[^>]*)\/>/gi, + + // Support: IE 10-11, Edge 10240+ + // In IE/Edge using regex groups here causes severe slowdowns. + // See https://connect.microsoft.com/IE/feedback/details/1736512/ + rnoInnerhtml = /\s*$/g; + +// Manipulating tables requires a tbody +function manipulationTarget( elem, content ) { + return jQuery.nodeName( elem, "table" ) && + jQuery.nodeName( content.nodeType !== 11 ? content : content.firstChild, "tr" ) ? + + elem.getElementsByTagName( "tbody" )[ 0 ] || + elem.appendChild( elem.ownerDocument.createElement( "tbody" ) ) : + elem; +} + +// Replace/restore the type attribute of script elements for safe DOM manipulation +function disableScript( elem ) { + elem.type = ( elem.getAttribute( "type" ) !== null ) + "/" + elem.type; + return elem; +} +function restoreScript( elem ) { + var match = rscriptTypeMasked.exec( elem.type ); + + if ( match ) { + elem.type = match[ 1 ]; + } else { + elem.removeAttribute( "type" ); + } + + return elem; +} + +function cloneCopyEvent( src, dest ) { + var i, l, type, pdataOld, pdataCur, udataOld, udataCur, events; + + if ( dest.nodeType !== 1 ) { + return; + } + + // 1. Copy private data: events, handlers, etc. + if ( dataPriv.hasData( src ) ) { + pdataOld = dataPriv.access( src ); + pdataCur = dataPriv.set( dest, pdataOld ); + events = pdataOld.events; + + if ( events ) { + delete pdataCur.handle; + pdataCur.events = {}; + + for ( type in events ) { + for ( i = 0, l = events[ type ].length; i < l; i++ ) { + jQuery.event.add( dest, type, events[ type ][ i ] ); + } + } + } + } + + // 2. Copy user data + if ( dataUser.hasData( src ) ) { + udataOld = dataUser.access( src ); + udataCur = jQuery.extend( {}, udataOld ); + + dataUser.set( dest, udataCur ); + } +} + +// Fix IE bugs, see support tests +function fixInput( src, dest ) { + var nodeName = dest.nodeName.toLowerCase(); + + // Fails to persist the checked state of a cloned checkbox or radio button. + if ( nodeName === "input" && rcheckableType.test( src.type ) ) { + dest.checked = src.checked; + + // Fails to return the selected option to the default selected state when cloning options + } else if ( nodeName === "input" || nodeName === "textarea" ) { + dest.defaultValue = src.defaultValue; + } +} + +function domManip( collection, args, callback, ignored ) { + + // Flatten any nested arrays + args = concat.apply( [], args ); + + var fragment, first, scripts, hasScripts, node, doc, + i = 0, + l = collection.length, + iNoClone = l - 1, + value = args[ 0 ], + isFunction = jQuery.isFunction( value ); + + // We can't cloneNode fragments that contain checked, in WebKit + if ( isFunction || + ( l > 1 && typeof value === "string" && + !support.checkClone && rchecked.test( value ) ) ) { + return collection.each( function( index ) { + var self = collection.eq( index ); + if ( isFunction ) { + args[ 0 ] = value.call( this, index, self.html() ); + } + domManip( self, args, callback, ignored ); + } ); + } + + if ( l ) { + fragment = buildFragment( args, collection[ 0 ].ownerDocument, false, collection, ignored ); + first = fragment.firstChild; + + if ( fragment.childNodes.length === 1 ) { + fragment = first; + } + + // Require either new content or an interest in ignored elements to invoke the callback + if ( first || ignored ) { + scripts = jQuery.map( getAll( fragment, "script" ), disableScript ); + hasScripts = scripts.length; + + // Use the original fragment for the last item + // instead of the first because it can end up + // being emptied incorrectly in certain situations (#8070). + for ( ; i < l; i++ ) { + node = fragment; + + if ( i !== iNoClone ) { + node = jQuery.clone( node, true, true ); + + // Keep references to cloned scripts for later restoration + if ( hasScripts ) { + + // Support: Android<4.1, PhantomJS<2 + // push.apply(_, arraylike) throws on ancient WebKit + jQuery.merge( scripts, getAll( node, "script" ) ); + } + } + + callback.call( collection[ i ], node, i ); + } + + if ( hasScripts ) { + doc = scripts[ scripts.length - 1 ].ownerDocument; + + // Reenable scripts + jQuery.map( scripts, restoreScript ); + + // Evaluate executable scripts on first document insertion + for ( i = 0; i < hasScripts; i++ ) { + node = scripts[ i ]; + if ( rscriptType.test( node.type || "" ) && + !dataPriv.access( node, "globalEval" ) && + jQuery.contains( doc, node ) ) { + + if ( node.src ) { + + // Optional AJAX dependency, but won't run scripts if not present + if ( jQuery._evalUrl ) { + jQuery._evalUrl( node.src ); + } + } else { + jQuery.globalEval( node.textContent.replace( rcleanScript, "" ) ); + } + } + } + } + } + } + + return collection; +} + +function remove( elem, selector, keepData ) { + var node, + nodes = selector ? jQuery.filter( selector, elem ) : elem, + i = 0; + + for ( ; ( node = nodes[ i ] ) != null; i++ ) { + if ( !keepData && node.nodeType === 1 ) { + jQuery.cleanData( getAll( node ) ); + } + + if ( node.parentNode ) { + if ( keepData && jQuery.contains( node.ownerDocument, node ) ) { + setGlobalEval( getAll( node, "script" ) ); + } + node.parentNode.removeChild( node ); + } + } + + return elem; +} + +jQuery.extend( { + htmlPrefilter: function( html ) { + return html.replace( rxhtmlTag, "<$1>" ); + }, + + clone: function( elem, dataAndEvents, deepDataAndEvents ) { + var i, l, srcElements, destElements, + clone = elem.cloneNode( true ), + inPage = jQuery.contains( elem.ownerDocument, elem ); + + // Fix IE cloning issues + if ( !support.noCloneChecked && ( elem.nodeType === 1 || elem.nodeType === 11 ) && + !jQuery.isXMLDoc( elem ) ) { + + // We eschew Sizzle here for performance reasons: http://jsperf.com/getall-vs-sizzle/2 + destElements = getAll( clone ); + srcElements = getAll( elem ); + + for ( i = 0, l = srcElements.length; i < l; i++ ) { + fixInput( srcElements[ i ], destElements[ i ] ); + } + } + + // Copy the events from the original to the clone + if ( dataAndEvents ) { + if ( deepDataAndEvents ) { + srcElements = srcElements || getAll( elem ); + destElements = destElements || getAll( clone ); + + for ( i = 0, l = srcElements.length; i < l; i++ ) { + cloneCopyEvent( srcElements[ i ], destElements[ i ] ); + } + } else { + cloneCopyEvent( elem, clone ); + } + } + + // Preserve script evaluation history + destElements = getAll( clone, "script" ); + if ( destElements.length > 0 ) { + setGlobalEval( destElements, !inPage && getAll( elem, "script" ) ); + } + + // Return the cloned set + return clone; + }, + + cleanData: function( elems ) { + var data, elem, type, + special = jQuery.event.special, + i = 0; + + for ( ; ( elem = elems[ i ] ) !== undefined; i++ ) { + if ( acceptData( elem ) ) { + if ( ( data = elem[ dataPriv.expando ] ) ) { + if ( data.events ) { + for ( type in data.events ) { + if ( special[ type ] ) { + jQuery.event.remove( elem, type ); + + // This is a shortcut to avoid jQuery.event.remove's overhead + } else { + jQuery.removeEvent( elem, type, data.handle ); + } + } + } + + // Support: Chrome <= 35-45+ + // Assign undefined instead of using delete, see Data#remove + elem[ dataPriv.expando ] = undefined; + } + if ( elem[ dataUser.expando ] ) { + + // Support: Chrome <= 35-45+ + // Assign undefined instead of using delete, see Data#remove + elem[ dataUser.expando ] = undefined; + } + } + } + } +} ); + +jQuery.fn.extend( { + + // Keep domManip exposed until 3.0 (gh-2225) + domManip: domManip, + + detach: function( selector ) { + return remove( this, selector, true ); + }, + + remove: function( selector ) { + return remove( this, selector ); + }, + + text: function( value ) { + return access( this, function( value ) { + return value === undefined ? + jQuery.text( this ) : + this.empty().each( function() { + if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) { + this.textContent = value; + } + } ); + }, null, value, arguments.length ); + }, + + append: function() { + return domManip( this, arguments, function( elem ) { + if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) { + var target = manipulationTarget( this, elem ); + target.appendChild( elem ); + } + } ); + }, + + prepend: function() { + return domManip( this, arguments, function( elem ) { + if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) { + var target = manipulationTarget( this, elem ); + target.insertBefore( elem, target.firstChild ); + } + } ); + }, + + before: function() { + return domManip( this, arguments, function( elem ) { + if ( this.parentNode ) { + this.parentNode.insertBefore( elem, this ); + } + } ); + }, + + after: function() { + return domManip( this, arguments, function( elem ) { + if ( this.parentNode ) { + this.parentNode.insertBefore( elem, this.nextSibling ); + } + } ); + }, + + empty: function() { + var elem, + i = 0; + + for ( ; ( elem = this[ i ] ) != null; i++ ) { + if ( elem.nodeType === 1 ) { + + // Prevent memory leaks + jQuery.cleanData( getAll( elem, false ) ); + + // Remove any remaining nodes + elem.textContent = ""; + } + } + + return this; + }, + + clone: function( dataAndEvents, deepDataAndEvents ) { + dataAndEvents = dataAndEvents == null ? false : dataAndEvents; + deepDataAndEvents = deepDataAndEvents == null ? dataAndEvents : deepDataAndEvents; + + return this.map( function() { + return jQuery.clone( this, dataAndEvents, deepDataAndEvents ); + } ); + }, + + html: function( value ) { + return access( this, function( value ) { + var elem = this[ 0 ] || {}, + i = 0, + l = this.length; + + if ( value === undefined && elem.nodeType === 1 ) { + return elem.innerHTML; + } + + // See if we can take a shortcut and just use innerHTML + if ( typeof value === "string" && !rnoInnerhtml.test( value ) && + !wrapMap[ ( rtagName.exec( value ) || [ "", "" ] )[ 1 ].toLowerCase() ] ) { + + value = jQuery.htmlPrefilter( value ); + + try { + for ( ; i < l; i++ ) { + elem = this[ i ] || {}; + + // Remove element nodes and prevent memory leaks + if ( elem.nodeType === 1 ) { + jQuery.cleanData( getAll( elem, false ) ); + elem.innerHTML = value; + } + } + + elem = 0; + + // If using innerHTML throws an exception, use the fallback method + } catch ( e ) {} + } + + if ( elem ) { + this.empty().append( value ); + } + }, null, value, arguments.length ); + }, + + replaceWith: function() { + var ignored = []; + + // Make the changes, replacing each non-ignored context element with the new content + return domManip( this, arguments, function( elem ) { + var parent = this.parentNode; + + if ( jQuery.inArray( this, ignored ) < 0 ) { + jQuery.cleanData( getAll( this ) ); + if ( parent ) { + parent.replaceChild( elem, this ); + } + } + + // Force callback invocation + }, ignored ); + } +} ); + +jQuery.each( { + appendTo: "append", + prependTo: "prepend", + insertBefore: "before", + insertAfter: "after", + replaceAll: "replaceWith" +}, function( name, original ) { + jQuery.fn[ name ] = function( selector ) { + var elems, + ret = [], + insert = jQuery( selector ), + last = insert.length - 1, + i = 0; + + for ( ; i <= last; i++ ) { + elems = i === last ? this : this.clone( true ); + jQuery( insert[ i ] )[ original ]( elems ); + + // Support: QtWebKit + // .get() because push.apply(_, arraylike) throws + push.apply( ret, elems.get() ); + } + + return this.pushStack( ret ); + }; +} ); + + +var iframe, + elemdisplay = { + + // Support: Firefox + // We have to pre-define these values for FF (#10227) + HTML: "block", + BODY: "block" + }; + +/** + * Retrieve the actual display of a element + * @param {String} name nodeName of the element + * @param {Object} doc Document object + */ + +// Called only from within defaultDisplay +function actualDisplay( name, doc ) { + var elem = jQuery( doc.createElement( name ) ).appendTo( doc.body ), + + display = jQuery.css( elem[ 0 ], "display" ); + + // We don't have any data stored on the element, + // so use "detach" method as fast way to get rid of the element + elem.detach(); + + return display; +} + +/** + * Try to determine the default display value of an element + * @param {String} nodeName + */ +function defaultDisplay( nodeName ) { + var doc = document, + display = elemdisplay[ nodeName ]; + + if ( !display ) { + display = actualDisplay( nodeName, doc ); + + // If the simple way fails, read from inside an iframe + if ( display === "none" || !display ) { + + // Use the already-created iframe if possible + iframe = ( iframe || jQuery( "