From 30ebab1c06f69fa4809dfc90adcf6dc1f681ff3f Mon Sep 17 00:00:00 2001 From: rikkertkoppes Date: Sat, 22 Nov 2014 06:56:09 +0100 Subject: [PATCH 01/29] fix test setup (nothing serious) --- spec/views/scoresSpec.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/spec/views/scoresSpec.js b/spec/views/scoresSpec.js index ec392408..d42ac50c 100644 --- a/spec/views/scoresSpec.js +++ b/spec/views/scoresSpec.js @@ -4,16 +4,18 @@ describe('scores', function() { 'services/log': logMock }); - var $scope, controller, scoresMock; + var $scope, controller, scoresMock, teamsMock; beforeEach(function() { scoresMock = createScoresMock(); + teamsMock = createTeamsMock(); angular.mock.module(module.name); angular.mock.inject(function($controller, $rootScope) { $scope = $rootScope.$new(); controller = $controller('scoresCtrl', { '$scope': $scope, - '$scores': scoresMock + '$scores': scoresMock, + '$teams': teamsMock }); }); }); From 6987e05bbc8e65125c13ea6a13b8d6ac4b8894c4 Mon Sep 17 00:00:00 2001 From: Martin Poelstra Date: Sun, 23 Nov 2014 16:48:36 +0100 Subject: [PATCH 02/29] ranking: Add highest score to CSV export, closes #93. --- src/js/views/ranking.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/js/views/ranking.js b/src/js/views/ranking.js index fd04157f..7d7a66da 100644 --- a/src/js/views/ranking.js +++ b/src/js/views/ranking.js @@ -86,9 +86,10 @@ define('views/ranking',[ entry.rank, entry.team.number, entry.team.name, + entry.highest, ].concat(entry.scores); }); - var header = ["Rank", "Team Number", "Team Name"]; + var header = ["Rank", "Team Number", "Team Name", "Highest"]; var stage = $stages.get(stageId); header = header.concat(stage.$rounds.map(function(round) { return "Round " + round; })); rows.unshift(header); From 27f861be743aceffe904eab03b72645a73089325 Mon Sep 17 00:00:00 2001 From: Martin Poelstra Date: Sun, 23 Nov 2014 16:49:30 +0100 Subject: [PATCH 03/29] ranking: Re-organize to allow testing, add comments, no functional changes. --- src/js/views/ranking.js | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/src/js/views/ranking.js b/src/js/views/ranking.js index 7d7a66da..41aec42f 100644 --- a/src/js/views/ranking.js +++ b/src/js/views/ranking.js @@ -61,9 +61,14 @@ define('views/ranking',[ return new Array($scope.maxRounds() - stage.$rounds.length); }; - $scope.csvdata = {}; - $scope.csvname = {}; + // Data for CSV export links, indexed by stage ID + $scope.csvdata = {}; // CSV data itself + $scope.csvname = {}; // Filenames suggested to user + // Convert a 2D matrix to a CSV string. + // All cells are converted to strings and fully quoted, + // except null or undefined cells, which are passed as empty + // values (without quotes). function toCSV(rows) { return rows.map(function(row) { return row.map(function(col) { @@ -73,14 +78,18 @@ define('views/ranking',[ } return '"' + String(col).replace(/"/gi, '""') + '"'; }).join(","); - }).join("\r\n"); + }).join("\r\n"); // Use Windows line-endings, to make it Notepad-friendly } - $scope.$watch("scoreboard", function() { + /** + * Rebuild CSV data (contents and filenames) of given scoreboard. + * @param scoreboard Per-stage ranking as present in e.g. $scores.scoreboard. + */ + $scope.rebuildCSV = function(scoreboard) { $scope.csvdata = {}; $scope.csvname = {}; - Object.keys($scores.scoreboard).forEach(function(stageId) { - var ranking = $scores.scoreboard[stageId]; + Object.keys(scoreboard).forEach(function(stageId) { + var ranking = scoreboard[stageId]; var rows = ranking.map(function(entry) { return [ entry.rank, @@ -96,6 +105,11 @@ define('views/ranking',[ $scope.csvname[stageId] = encodeURIComponent("ranking_" + stageId + ".csv"); $scope.csvdata[stageId] = "data:text/csv;charset=utf-8," + encodeURIComponent(toCSV(rows)); }); + } + + // Rebuild CSV data and filenames when scoreboard is updated + $scope.$watch("scoreboard", function() { + $scope.rebuildCSV($scores.scoreboard); }, true); $scope.stages = $stages.stages; From 616f8fd51f952888e6cea7ae7cdc07454a87aad2 Mon Sep 17 00:00:00 2001 From: Martin Poelstra Date: Sun, 23 Nov 2014 17:06:02 +0100 Subject: [PATCH 04/29] scoresheet: Remove no longer necessary dependency on $scores, helps #102. --- spec/views/scoresheetSpec.js | 1 - src/js/views/scoresheet.js | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/spec/views/scoresheetSpec.js b/spec/views/scoresheetSpec.js index 60b5673c..5a45c85c 100644 --- a/spec/views/scoresheetSpec.js +++ b/spec/views/scoresheetSpec.js @@ -21,7 +21,6 @@ describe('scoresheet',function() { '$scope': $scope, '$fs': fsMock, '$settings': settingsMock, - '$scores': {}, '$stages': {}, '$modal': {}, '$teams': {}, diff --git a/src/js/views/scoresheet.js b/src/js/views/scoresheet.js index 91bc9aff..899f38d3 100644 --- a/src/js/views/scoresheet.js +++ b/src/js/views/scoresheet.js @@ -14,8 +14,8 @@ define('views/scoresheet',[ var moduleName = 'scoresheet'; return angular.module(moduleName, []).controller(moduleName + 'Ctrl', [ - '$scope','$fs','$scores','$stages','$settings','$modal','$challenge','$window','$q','$teams', - function($scope,$fs,$scores,$stages,$settings,$modal,$challenge,$window,$q,$teams) { + '$scope','$fs','$stages','$settings','$modal','$challenge','$window','$q','$teams', + function($scope,$fs,$stages,$settings,$modal,$challenge,$window,$q,$teams) { log('init scoresheet ctrl'); // Set up defaults From 01533c8b987b28e494cf495eaf813d3db603f7c5 Mon Sep 17 00:00:00 2001 From: Martin Poelstra Date: Sun, 23 Nov 2014 17:31:56 +0100 Subject: [PATCH 05/29] stagesMock: Add get() method, update $rounds property. --- spec/mocks/stagesMock.js | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/spec/mocks/stagesMock.js b/spec/mocks/stagesMock.js index 6c7c234b..55b2ca3b 100644 --- a/spec/mocks/stagesMock.js +++ b/spec/mocks/stagesMock.js @@ -1,9 +1,19 @@ var stagesMock = { stages: [ - { id: "practice", name: "Oefenrondes", rounds: 2, _rounds: [1, 2] }, - { id: "qualifying", name: "Voorrondes", rounds: 3, _rounds: [1, 2, 3] }, - { id: "quarter", name: "Kwart finales", rounds: 0, _rounds: [] }, - { id: "semi", name: "Halve finales", rounds: 0, _rounds: [] }, - { id: "final", name: "Finale", rounds: 1, _rounds: [1] }, - ] + { id: "practice", name: "Oefenrondes", rounds: 2, $rounds: [1, 2] }, + { id: "qualifying", name: "Voorrondes", rounds: 3, $rounds: [1, 2, 3] }, + { id: "quarter", name: "Kwart finales", rounds: 0, $rounds: [] }, + { id: "semi", name: "Halve finales", rounds: 0, $rounds: [] }, + { id: "final", name: "Finale", rounds: 1, $rounds: [1] }, + ], + get: function(id) { + var stages = stagesMock.stages; + var i; + for (i = 0; i < stages.length; i++) { + if (stages[i].id === id) { + return stages[i]; + } + } + throw new Error("unknown stage"); + } } From eab4ce4c39828c4a5b9f9a3e72a6922ac2cdadaa Mon Sep 17 00:00:00 2001 From: Martin Poelstra Date: Sun, 23 Nov 2014 17:32:45 +0100 Subject: [PATCH 06/29] rankingSpec: Add unit test for CSV export. --- spec/views/rankingSpec.js | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/spec/views/rankingSpec.js b/spec/views/rankingSpec.js index 8355bc88..4a0c9dc1 100644 --- a/spec/views/rankingSpec.js +++ b/spec/views/rankingSpec.js @@ -54,4 +54,22 @@ describe('ranking', function() { }) }); + describe('export',function() { + it('should generate CSV data and filenames',function() { + expect($scope.csvname).toEqual({}); + expect($scope.csvdata).toEqual({}); + $scope.rebuildCSV({ + 'qualifying': [ + { rank: 1, team: { name: "foo", number: 123 }, highest: 10, scores: [0, 10, 5] }, + { rank: 1, team: { name: "\"bar\"", number: 456 }, highest: 10, scores: [10, 0, 5] } + ] + }); + expect($scope.csvname["qualifying"]).toEqual("ranking_qualifying.csv"); + expect($scope.csvdata["qualifying"]).toEqual("data:text/csv;charset=utf-8," + encodeURIComponent([ + '"Rank","Team Number","Team Name","Highest","Round 1","Round 2","Round 3"', + '"1","123","foo","10","0","10","5"', + '"1","456","""bar""","10","10","0","5"', + ].join("\r\n"))); + }); + }); }); From bd5f36f277ac9c87dba4901a311dc81ef860e229 Mon Sep 17 00:00:00 2001 From: robvw Date: Mon, 24 Nov 2014 02:43:04 +0100 Subject: [PATCH 07/29] Create 2014_nl_NL no-enum.xml Remove all tags (in favour of ) to get rid of spinners in rendered view (as per #104). Bumped version number (since it's for the same challenge), but committed under a different filename (since it's a hack, not an actual improvement). --- challenges/xml/2014_nl_NL no-enum.xml | 548 ++++++++++++++++++++++++++ 1 file changed, 548 insertions(+) create mode 100644 challenges/xml/2014_nl_NL no-enum.xml diff --git a/challenges/xml/2014_nl_NL no-enum.xml b/challenges/xml/2014_nl_NL no-enum.xml new file mode 100644 index 00000000..f0ae2de0 --- /dev/null +++ b/challenges/xml/2014_nl_NL no-enum.xml @@ -0,0 +1,548 @@ + + + + + + Ja + Nee + + 0 + 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + + Reverse Engineering + De zichtbare situatie aan het einde van de wedstrijd:
  • Jullie mand is in de basis.
  • Het model raakt de witte cirkel rond het projectonderwijs-model aan.
  • Jullie hebben een model gemaakt ‘identiek’ aan het model dat het andere team in jullie mand heeft gedaan. De verbindingen tussen de elementen moeten hetzelfde zijn, maar elementen mogen wel ‘gedraaid’ zitten.
  • Het model is in de basis.
Vereiste methode en beperkingen:
  • Geen.
]]>
+ Mand in de basis + Jullie model ligt in de basis en is ‘identiek’ + + Deuren openen + De zichtbare situatie aan het einde van de wedstrijd:
  • De deur moet ver genoeg geopend zijn zodat de scheidsrechter dit kan zien.
Vereiste methode en beperkingen:
  • De hendel moet omlaag gedrukt zijn.
]]>
+ Deur geopend door de hendel omlaag te drukken + + Projectonderwijs + De zichtbare situatie aan het einde van de wedstrijd:
  • De lussen (welke symbool staan voor kennis en vaardigheden) hangen aan de weegschaal zoals getoond.
Vereiste methode en beperkingen:
  • Geen.
]]>
+ Lussen aan de weegschaal + + Stagelopen + De zichtbare situatie aan het einde van de wedstrijd:
  • De LEGO-poppetjes zijn beiden verbonden (op een manier naar keuze) aan een model dat jullie ontwerpen en meenemen. Dit model stelt een vaardigheid, prestatie, carrière of hobby voor dat een speciale betekenis voor jullie team heeft.
  • Het model raakt de witte cirkel rond het projectonderwijs-model aan.
  • Het model is niet in de basis.
  • Het vastmaken van missiemodellen is normaal niet toegestaan vanwege regel 39.4, deze missie is daar een uitzondering op.
  • Het eigen model mag simpel, of complex zijn, het mag primitief of realistisch zijn, de keuze is aan jullie. De keuze wat voor model jullie bouwen, heeft geen invloed op de score.
Vereiste methode en beperkingen:
  • Geen.
]]>
+ Model getoond aan de scheidsrechter + Raakt de cirkel, niet in de basis en poppetjes verbonden + Model moet getoond zijn voordat het de cirkel kan aanraken + + Zoekmachine + De zichtbare situatie aan het einde van de wedstrijd:
  • Het kleurenwiel heeft minimaal een keer gedraaid.
  • Als één kleur verschijnt in het witte frame, dan raakt de lus van de zichtbare kleur het model niet meer aan.
  • Als twee kleuren verschijnen in het witte venster, dan is de lus van de kleur die niet zichtbaar is in het venster, de lus die het model niet meer raakt.
  • Beide lussen die niet verwijderd dienen te worden raken via ‘hun’ gaten het model aan.
Vereiste methode en beperkingen:
  • Alleen de beweging van de schuif heeft het kleurenwiel in beweging gebracht.
]]>
+ Alleen de schuif heeft het wiel 1+ keer rondgedraaid + Alleen de juiste lus is verwijderd + + Sport + De zichtbare situatie aan het einde van de wedstrijd:
  • De bal raakt de mat in het doel.
Vereiste methode en beperkingen:
  • Alle onderdelen die met het schot te maken hebben waren volledig ten noordoosten van de ‘schietlijn’ op het moment dat de bal werd losgelaten richting het doel.
]]>
+ Schot genomen vanuit positie Noord-Oost van de lijn + Bal raakt de mat in het doel aan het eind van de wedstrijd + Bal moet eerst geschoten zijn voordat het in het doel kan zijn + + Robotwedstrijden + De zichtbare situatie aan het einde van de wedstrijd:
  • Het blauw-geel-rode robotelement (model) is geïnstalleerd in de robotarm zoals zichtbaar op de afbeelding.
  • De lus raakt niet langer de robotarm aan.
Vereiste methode en beperkingen:
  • Geen strategisch object raakt de robotarm aan.
  • De lus werd alleen door het gebruik van de zwarte schuif losgemaakt.
]]>
+ Alleen het robotelement is geïnstalleerd in de robotarm + Lus raakt de robotarm niet aan + + Gebruik de juiste zintuigen en leerstijlen + De zichtbare situatie aan het einde van de wedstrijd:
  • De lus raakt het zintuigen model niet meer aan.
Vereiste methode en beperkingen:
  • De lus werd alleen door het gebruik van de schuif losgemaakt.
]]>
+ Lus raakt het model niet aan + + Leren/communiceren op afstand + De zichtbare situatie aan het einde van de wedstrijd:
  • Geen.
Vereiste methode en beperkingen:
  • De scheidsrechter heeft gezien dat de schuif door de robot westwaarts is verplaatst.
]]>
+ Scheids zag de robot de schuif verplaatsen + + Outside the Box denken + De zichtbare situatie aan het einde van de wedstrijd:
  • Het ‘idee-model’ raakt niet langer het ‘doos-model’ aan.
  • Als het ‘idee-model’ het ‘doos-model’ niet meer aanraakt, is de afbeelding van de gloeilamp van bovenaf zichtbaar.
Vereiste methode en beperkingen:
  • Het ‘doos-model’ is nooit in de basis geweest.
]]>
+ Idee-model raakt de doos niet, Doos niet in basis geweest, Lamp is naar boven gericht + Idee-model raakt de doos niet, Doos niet in basis geweest, Lamp is naar beneden gericht + De lamp kan niet tegelijk naar boven en beneden gericht zijn + + Gemeenschappelijk leren + De zichtbare situatie aan het einde van de wedstrijd:
  • De ‘kennis & vaardigheden lus’ raakt het gemeenschapsmodel niet meer aan.
Vereiste methode en beperkingen:
  • Geen.
]]>
+ Lus raakt het model niet aan + + Cloud toegang + De zichtbare situatie aan het einde van de wedstrijd:
  • De SD-kaart staat omhoog.
Vereiste methode en beperkingen:
  • De juiste “key” is in de Cloud geplaatst.
]]>
+ SD card staat omhoog omdat de juiste "key" is ingebracht + + Betrokkenheid + De zichtbare situatie aan het einde van de wedstrijd:
  • Het gele gedeelte is naar het zuiden verplaatst.
  • Het rad is duidelijk met de klok mee gedraaid ten opzichte van de start positie. Zie het overzicht voor de score.
Vereiste methode en beperkingen:
  • De wijzer mag alleen verplaatst worden doordat de robot aan het rad te draait.
  • De robot mag het rad maar een keer 180⁰ draaien, per keer dat de basis wordt verlaten. De scheidsrechter zal extra draaiingen ongedaan maken
]]>
+ Gele gedeelte is naar het zuiden verplaatst + Aangewezen kleur + Klikken voorbij de kleur + NVT + Rood 10% + Oranje 16% + Groen 22% + Blauw 28% + Rood 34% + Blauw 40% + Groen 46% + Oranje 52% + Rood 58% + 0 + 1 + 2 + 3 + 4 + 5 + Er moet of twee keer "NVT" of twee keer een waarde worden gekozen + De wijzer blijft op "NVT" staan als het gele gedeelte niet geactiveerd is + De wijzer kan niet zover draaien + + Flexibiliteit + De zichtbare situatie aan het einde van de wedstrijd:
  • Het model is 90⁰ gedraaid tegen de richting van de klok in ten opzichte van de beginpositie.
Vereiste methode en beperkingen:
  • Geen.
]]>
+ Model is 90 graden tegen de klok in gedraaid + + Strafpunten + + Robot, rommel of uitvouwstrafpunten +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
From fefb7c7e4862063bbc8799cb7846b5a90df56083 Mon Sep 17 00:00:00 2001 From: rikkertkoppes Date: Mon, 24 Nov 2014 12:19:56 +0100 Subject: [PATCH 08/29] Add basic authentication, closes #107 --- localserver.js | 9 +++++++++ readme.md | 5 +++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/localserver.js b/localserver.js index 15d83109..269a7d90 100644 --- a/localserver.js +++ b/localserver.js @@ -5,9 +5,18 @@ var mkdirp = require("mkdirp"); var dirname = require('path').dirname; var argv = require('minimist')(process.argv.slice(2)); var port = argv.p||1390; +var basicAuth = argv.u; app.use(express.static('src')); +//set up basic authentication +if (basicAuth) { + var pair = basicAuth.split(':'); + var user = pair[0]; + var pass = pair[1]; + app.use(express.basicAuth(user, pass)); +} + //allow cors headers app.use(function(req, res, next) { res.header('Access-Control-Allow-Origin', '*'); diff --git a/readme.md b/readme.md index 8d647bab..f1e4273e 100644 --- a/readme.md +++ b/readme.md @@ -35,13 +35,14 @@ For iOS, see [Building for iOS](https://github.com/FirstLegoLeague/fllscoring/wi To build js challenge files from the xml description files, use - node tools\buildchallenge.js challenges\xml\2014.xml > challenges\js\2014.js + node tools\buildchallenge.js challenges\xml\2014.xml > challenges\js\2014.js Run local -------- - `node localserver.js` then open [localhost:1390](http://localhost:1390) - - to specify another port, use `node localserver.js -p 8000` + - to specify another port, use `node localserver.js -p 8000` + - to add basic authentication, use `node localserver.js -u username:password` Testing ------- From 6b185eba9d9b90ae529f3765f399f0777d5c275c Mon Sep 17 00:00:00 2001 From: rikkertkoppes Date: Mon, 24 Nov 2014 12:21:59 +0100 Subject: [PATCH 09/29] Include bower angular-bootstrap component, closes #108 --- .gitignore | 1 + .../angular-bootstrap/ui-bootstrap-tpls.js | 4167 +++++++++++++++++ .../ui-bootstrap-tpls.min.js | 10 + .../angular-bootstrap/ui-bootstrap.js | 3857 +++++++++++++++ .../angular-bootstrap/ui-bootstrap.min.js | 9 + 5 files changed, 8044 insertions(+) create mode 100644 src/components/angular-bootstrap/ui-bootstrap-tpls.js create mode 100644 src/components/angular-bootstrap/ui-bootstrap-tpls.min.js create mode 100644 src/components/angular-bootstrap/ui-bootstrap.js create mode 100644 src/components/angular-bootstrap/ui-bootstrap.min.js diff --git a/.gitignore b/.gitignore index 3063a12a..8b37dd85 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,7 @@ src/components/*/* !src/components/angular/angular*.js !src/components/angular-sanitize/angular-sanitize*.js !src/components/angular-mocks/angular*.js +!src/components/angular-bootstrap/*.js !src/components/bootstrap-css/css !src/components/bootstrap-css/img diff --git a/src/components/angular-bootstrap/ui-bootstrap-tpls.js b/src/components/angular-bootstrap/ui-bootstrap-tpls.js new file mode 100644 index 00000000..260c2769 --- /dev/null +++ b/src/components/angular-bootstrap/ui-bootstrap-tpls.js @@ -0,0 +1,4167 @@ +/* + * angular-ui-bootstrap + * http://angular-ui.github.io/bootstrap/ + + * Version: 0.11.2 - 2014-09-26 + * 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) { + + return { + link: function (scope, element, attrs) { + + 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; + } + } + } + + function expand() { + if (initialAnimSkip) { + initialAnimSkip = false; + expandDone(); + } else { + element.removeClass('collapse').addClass('collapsing'); + doTransition({ height: element[0].scrollHeight + 'px' }).then(expandDone); + } + } + + function expandDone() { + element.removeClass('collapsing'); + element.addClass('collapse in'); + element.css({height: 'auto'}); + } + + 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); + } + } + + function collapseDone() { + element.removeClass('collapsing'); + element.addClass('collapse'); + } + + scope.$watch(attrs.collapse, function (shouldCollapse) { + if (shouldCollapse) { + collapse(); + } else { + expand(); + } + }); + } + }; + }]); + +angular.module('ui.bootstrap.accordion', ['ui.bootstrap.collapse']) + +.constant('accordionConfig', { + closeOthers: true +}) + +.controller('AccordionController', ['$scope', '$attrs', 'accordionConfig', 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 ) { + group.isOpen = false; + } + }); + } + }; + + // This is called from the accordion-group directive to add itself to the accordion + this.addGroup = function(groupScope) { + var that = this; + this.groups.push(groupScope); + + groupScope.$on('$destroy', function (event) { + that.removeGroup(groupScope); + }); + }; + + // 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 ) { + 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 () { + return { + restrict:'EA', + controller:'AccordionController', + transclude: true, + replace: false, + templateUrl: 'template/accordion/accordion.html' + }; +}) + +// The accordion-group directive indicates a block of html that will expand and collapse in an accordion +.directive('accordionGroup', 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 + replace: true, // The element containing the directive will be replaced with the template + templateUrl:'template/accordion/accordion-group.html', + scope: { + heading: '@', // Interpolate the heading attribute onto this scope + isOpen: '=?', + isDisabled: '=?' + }, + controller: function() { + this.setHeading = function(element) { + this.heading = element; + }; + }, + link: function(scope, element, attrs, accordionCtrl) { + accordionCtrl.addGroup(scope); + + scope.$watch('isOpen', function(value) { + if ( value ) { + accordionCtrl.closeOthers(scope); + } + }); + + scope.toggleOpen = function() { + if ( !scope.isDisabled ) { + scope.isOpen = !scope.isOpen; + } + }; + } + }; +}) + +// Use accordion-heading below an accordion-group to provide a heading containing HTML +// +// Heading containing HTML - +// +.directive('accordionHeading', 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) { + // 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() {})); + } + }; +}) + +// 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() { + 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); + } + }); + } + }; +}); + +angular.module('ui.bootstrap.alert', []) + +.controller('AlertController', ['$scope', '$attrs', function ($scope, $attrs) { + $scope.closeable = 'close' in $attrs; +}]) + +.directive('alert', function () { + return { + restrict:'EA', + controller:'AlertController', + templateUrl:'template/alert/alert.html', + transclude:true, + replace:true, + scope: { + type: '@', + close: '&' + } + }; +}); + +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', { + activeClass: 'active', + toggleEvent: 'click' +}) + +.controller('ButtonsController', ['buttonConfig', function(buttonConfig) { + this.activeClass = buttonConfig.activeClass || 'active'; + this.toggleEvent = buttonConfig.toggleEvent || 'click'; +}]) + +.directive('btnRadio', function () { + return { + require: ['btnRadio', 'ngModel'], + controller: 'ButtonsController', + link: function (scope, element, attrs, ctrls) { + var buttonsCtrl = ctrls[0], ngModelCtrl = ctrls[1]; + + //model -> UI + ngModelCtrl.$render = function () { + element.toggleClass(buttonsCtrl.activeClass, angular.equals(ngModelCtrl.$modelValue, scope.$eval(attrs.btnRadio))); + }; + + //ui->model + element.bind(buttonsCtrl.toggleEvent, function () { + var isActive = element.hasClass(buttonsCtrl.activeClass); + + if (!isActive || angular.isDefined(attrs.uncheckable)) { + scope.$apply(function () { + ngModelCtrl.$setViewValue(isActive ? null : scope.$eval(attrs.btnRadio)); + ngModelCtrl.$render(); + }); + } + }); + } + }; +}) + +.directive('btnCheckbox', function () { + return { + require: ['btnCheckbox', 'ngModel'], + controller: 'ButtonsController', + link: function (scope, element, attrs, ctrls) { + var buttonsCtrl = ctrls[0], ngModelCtrl = ctrls[1]; + + function getTrueValue() { + return getCheckboxValue(attrs.btnCheckboxTrue, true); + } + + function getFalseValue() { + return getCheckboxValue(attrs.btnCheckboxFalse, false); + } + + function getCheckboxValue(attributeValue, defaultValue) { + var val = scope.$eval(attributeValue); + return angular.isDefined(val) ? val : defaultValue; + } + + //model -> UI + ngModelCtrl.$render = function () { + element.toggleClass(buttonsCtrl.activeClass, angular.equals(ngModelCtrl.$modelValue, getTrueValue())); + }; + + //ui->model + element.bind(buttonsCtrl.toggleEvent, function () { + scope.$apply(function () { + ngModelCtrl.$setViewValue(element.hasClass(buttonsCtrl.activeClass) ? getFalseValue() : getTrueValue()); + ngModelCtrl.$render(); + }); + }); + } + }; +}); + +/** +* @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) { + var self = this, + slides = self.slides = $scope.slides = [], + currentIndex = -1, + currentTimeout, isPlaying; + self.currentSlide = null; + + var destroyed = false; + /* direction: "prev" or "next" */ + self.select = $scope.select = function(nextSlide, direction) { + var nextIndex = slides.indexOf(nextSlide); + //Decide direction if it's not given + if (direction === undefined) { + direction = nextIndex > currentIndex ? '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; + } + }; + $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.isActive = function(slide) { + return self.currentSlide === slide; + }; + + $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.pause = function() { + if (!$scope.noPause) { + isPlaying = false; + resetTimer(); + } + }; + + 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; + } + }; + + 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 { + self.select(slides[index]); + } + } else if (currentIndex > index) { + currentIndex--; + } + }; + +}]) + +/** + * @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() { + return { + restrict: 'EA', + transclude: true, + replace: true, + controller: 'CarouselController', + require: 'carousel', + templateUrl: 'template/carousel/carousel.html', + scope: { + interval: '=', + noTransition: '=', + noPause: '=' + } + }; +}]) + +/** + * @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() { + return { + require: '^carousel', + restrict: 'EA', + transclude: true, + replace: true, + templateUrl: 'template/carousel/slide.html', + scope: { + active: '=?' + }, + link: function (scope, element, attrs, carouselCtrl) { + carouselCtrl.addSlide(scope, element); + //when the scope is destroyed then remove the slide from the current slides array + scope.$on('$destroy', function() { + carouselCtrl.removeSlide(scope); + }); + + scope.$watch('active', function(active) { + if (active) { + carouselCtrl.select(scope); + } + }); + } + }; +}); + +angular.module('ui.bootstrap.dateparser', []) + +.service('dateParser', ['$locale', 'orderByFilter', function($locale, orderByFilter) { + + this.parsers = {}; + + 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('|') + } + }; + + function createParser(format) { + var map = [], regex = format.split(''); + + angular.forEach(formatCodeToRegex, function(data, code) { + var index = format.indexOf(code); + + 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++) { + regex[i] = ''; + format[i] = '$'; + } + format = format.join(''); + + map.push({ index: index, apply: data.apply }); + } + }); + + return { + regex: new RegExp('^' + regex.join('') + '$'), + map: orderByFilter(map, 'index') + }; + } + + this.parse = function(input, format) { + if ( !angular.isString(input) || !format ) { + return input; + } + + format = $locale.DATETIME_FORMATS[format] || format; + + if ( !this.parsers[format] ) { + this.parsers[format] = createParser(format); + } + + var parser = this.parsers[format], + regex = parser.regex, + map = parser.map, + results = input.match(regex); + + 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.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); + } + + return dt; + } + }; + + // 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 ( month === 3 || month === 5 || month === 8 || month === 10) { + return date < 31; + } + + return true; + } +}]); + +angular.module('ui.bootstrap.position', []) + +/** + * 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) { + + function getStyle(el, cssprop) { + if (el.currentStyle) { //IE + return el.currentStyle[cssprop]; + } else if ($window.getComputedStyle) { + return $window.getComputedStyle(el)[cssprop]; + } + // 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; + } + + 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; + } + }; + }]); + +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: true, + startingDay: 0, + yearRange: 20, + minDate: null, + maxDate: null +}) + +.controller('DatepickerController', ['$scope', '$attrs', '$parse', '$interpolate', '$timeout', '$log', 'dateFilter', 'datepickerConfig', function($scope, $attrs, $parse, $interpolate, $timeout, $log, dateFilter, datepickerConfig) { + var self = this, + ngModelCtrl = { $setViewValue: angular.noop }; // nullModelCtrl; + + // 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]; + }); + + // Watchable date 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; + } + }); + + $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.isActive = function(dateObject) { + if (self.compare(dateObject.date, self.activeDate) === 0) { + $scope.activeDateId = dateObject.uid; + return true; + } + return false; + }; + + this.init = function( ngModelCtrl_ ) { + ngModelCtrl = ngModelCtrl_; + + ngModelCtrl.$render = function() { + self.render(); + }; + }; + + this.render = function() { + if ( ngModelCtrl.$modelValue ) { + var date = new Date( ngModelCtrl.$modelValue ), + 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.'); + } + ngModelCtrl.$setValidity('date', isValid); + } + this.refreshView(); + }; + + this.refreshView = function() { + if ( this.element ) { + this._refreshView(); + + var date = ngModelCtrl.$modelValue ? new Date(ngModelCtrl.$modelValue) : null; + ngModelCtrl.$setValidity('date-disabled', !date || (this.element && !this.isDisabled(date))); + } + }; + + this.createDateObject = function(date, format) { + var model = ngModelCtrl.$modelValue ? new Date(ngModelCtrl.$modelValue) : null; + return { + date: date, + label: dateFilter(date, format), + selected: model && this.compare(date, model) === 0, + disabled: this.isDisabled(date), + current: this.compare(date, new Date()) === 0 + }; + }; + + 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}))); + }; + + // Split array into smaller arrays + this.split = function(arr, size) { + var arrays = []; + while (arr.length > 0) { + arrays.push(arr.splice(0, size)); + } + 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 ); + ngModelCtrl.$render(); + } else { + self.activeDate = date; + $scope.datepickerMode = self.modes[ self.modes.indexOf( $scope.datepickerMode ) - 1 ]; + } + }; + + $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 ) { + direction = 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 ]; + }; + + // 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' }; + + var focusElement = function() { + $timeout(function() { + self.element[0].focus(); + }, 0 , false); + }; + + // Listen for focus requests from popup directive + $scope.$on('datepicker.focus', focusElement); + + $scope.keydown = function( evt ) { + var key = $scope.keys[evt.which]; + + if ( !key || evt.shiftKey || evt.altKey ) { + return; + } + + evt.preventDefault(); + evt.stopPropagation(); + + if (key === 'enter' || key === 'space') { + 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(); + } + }; +}]) + +.directive( 'datepicker', function () { + return { + restrict: 'EA', + replace: true, + templateUrl: 'template/datepicker/datepicker.html', + scope: { + datepickerMode: '=?', + dateDisabled: '&' + }, + require: ['datepicker', '?^ngModel'], + controller: 'DatepickerController', + link: function(scope, element, attrs, ctrls) { + var datepickerCtrl = ctrls[0], ngModelCtrl = ctrls[1]; + + if ( ngModelCtrl ) { + datepickerCtrl.init( ngModelCtrl ); + } + } + }; +}) + +.directive('daypicker', ['dateFilter', function (dateFilter) { + 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: '&' + }, + 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; + + 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 ); + }); + } + + scope.watchData = {}; + angular.forEach(['minDate', 'maxDate', 'datepickerMode'], function( key ) { + if ( attrs[key] ) { + var getAttribute = $parse(attrs[key]); + scope.$parent.$watch(getAttribute, function(value){ + scope.watchData[key] = value; + }); + datepickerEl.attr(cameltoDash(key), 'watchData.' + key); + + // Propagate changes from datepicker to outside + if ( key === 'datepickerMode' ) { + var setAttribute = getAttribute.assign; + scope.$watch('watchData.' + key, function(value, oldvalue) { + if ( value !== oldvalue ) { + setAttribute(scope.$parent, value); + } + }); + } + } + }); + 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); + // 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() { + $popup.remove(); + element.unbind('keydown', keydown); + $document.unbind('click', documentClickBind); + }); + } + }; +}]) + +.directive('datepickerPopupWrap', 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(); + }); + } + }; +}); + +angular.module('ui.bootstrap.dropdown', []) + +.constant('dropdownConfig', { + openClass: 'open' +}) + +.service('dropdownService', ['$document', function($document) { + var openScope = null; + + this.open = function( dropdownScope ) { + if ( !openScope ) { + $document.bind('click', closeDropdown); + $document.bind('keydown', escapeKeyBind); + } + + if ( openScope && openScope !== dropdownScope ) { + openScope.isOpen = false; + } + + openScope = dropdownScope; + }; + + this.close = function( dropdownScope ) { + if ( openScope === dropdownScope ) { + openScope = null; + $document.unbind('click', closeDropdown); + $document.unbind('keydown', escapeKeyBind); + } + }; + + var closeDropdown = function( evt ) { + var toggleElement = openScope.getToggleElement(); + if ( evt && toggleElement && toggleElement[0].contains(evt.target) ) { + return; + } + + openScope.$apply(function() { + openScope.isOpen = false; + }); + }; + + var escapeKeyBind = function( evt ) { + if ( evt.which === 27 ) { + openScope.focusToggleElement(); + closeDropdown(); + } + }; +}]) + +.controller('DropdownController', ['$scope', '$attrs', '$parse', 'dropdownConfig', 'dropdownService', '$animate', function($scope, $attrs, $parse, dropdownConfig, dropdownService, $animate) { + 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; + + this.init = function( element ) { + self.$element = element; + + if ( $attrs.isOpen ) { + getIsOpen = $parse($attrs.isOpen); + setIsOpen = getIsOpen.assign; + + $scope.$watch(getIsOpen, function(value) { + scope.isOpen = !!value; + }); + } + }; + + this.toggle = function( open ) { + return scope.isOpen = arguments.length ? !!open : !scope.isOpen; + }; + + // Allow other directives to watch status + this.isOpen = function() { + return scope.isOpen; + }; + + scope.getToggleElement = function() { + return self.toggleElement; + }; + + scope.focusToggleElement = function() { + if ( self.toggleElement ) { + self.toggleElement[0].focus(); + } + }; + + scope.$watch('isOpen', function( isOpen, wasOpen ) { + $animate[isOpen ? 'addClass' : 'removeClass'](self.$element, openClass); + + if ( isOpen ) { + scope.focusToggleElement(); + dropdownService.open( scope ); + } else { + dropdownService.close( scope ); + } + + setIsOpen($scope, isOpen); + if (angular.isDefined(isOpen) && isOpen !== wasOpen) { + toggleInvoker($scope, { open: !!isOpen }); + } + }); + + $scope.$on('$locationChangeSuccess', function() { + scope.isOpen = false; + }); + + $scope.$on('$destroy', function() { + scope.$destroy(); + }); +}]) + +.directive('dropdown', function() { + return { + restrict: 'CA', + controller: 'DropdownController', + link: function(scope, element, attrs, dropdownCtrl) { + dropdownCtrl.init( element ); + } + }; +}) + +.directive('dropdownToggle', function() { + return { + restrict: 'CA', + require: '?^dropdown', + link: function(scope, element, attrs, dropdownCtrl) { + if ( !dropdownCtrl ) { + return; + } + + dropdownCtrl.toggleElement = element; + + var toggleDropdown = function(event) { + event.preventDefault(); + + if ( !element.hasClass('disabled') && !attrs.disabled ) { + scope.$apply(function() { + dropdownCtrl.toggle(); + }); + } + }; + + element.bind('click', toggleDropdown); + + // WAI-ARIA + element.attr({ 'aria-haspopup': true, 'aria-expanded': false }); + scope.$watch(dropdownCtrl.isOpen, function( isOpen ) { + element.attr('aria-expanded', !!isOpen); + }); + + scope.$on('$destroy', function() { + element.unbind('click', toggleDropdown); + }); + } + }; +}); + +angular.module('ui.bootstrap.modal', ['ui.bootstrap.transition']) + +/** + * A helper, internal data structure that acts as a map but also allows getting / removing + * elements in the LIFO order + */ + .factory('$$stackedMap', function () { + return { + createNew: function () { + var stack = []; + + return { + add: function (key, value) { + stack.push({ + key: key, + value: value + }); + }, + get: function (key) { + for (var i = 0; i < stack.length; i++) { + if (key == stack[i].key) { + return stack[i]; + } + } + }, + keys: function() { + var keys = []; + for (var i = 0; i < stack.length; i++) { + keys.push(stack[i].key); + } + return keys; + }, + top: function () { + return stack[stack.length - 1]; + }, + remove: function (key) { + var idx = -1; + for (var i = 0; i < stack.length; i++) { + if (key == stack[i].key) { + idx = i; + break; + } + } + return stack.splice(idx, 1)[0]; + }, + removeTop: function () { + return stack.splice(stack.length - 1, 1)[0]; + }, + length: function () { + return stack.length; + } + }; + } + }; + }) + +/** + * A helper directive for the $modal service. It creates a backdrop element. + */ + .directive('modalBackdrop', ['$timeout', function ($timeout) { + return { + restrict: 'EA', + replace: true, + templateUrl: 'template/modal/backdrop.html', + link: function (scope, element, attrs) { + scope.backdropClass = attrs.backdropClass || ''; + + scope.animate = false; + + //trigger CSS transitions + $timeout(function () { + scope.animate = true; + }); + } + }; + }]) + + .directive('modalWindow', ['$modalStack', '$timeout', function ($modalStack, $timeout) { + return { + restrict: 'EA', + scope: { + index: '@', + animate: '=' + }, + replace: true, + transclude: true, + templateUrl: function(tElement, tAttrs) { + return tAttrs.templateUrl || 'template/modal/window.html'; + }, + link: function (scope, element, attrs) { + element.addClass(attrs.windowClass || ''); + scope.size = attrs.size; + + $timeout(function () { + // trigger CSS transitions + scope.animate = true; + + /** + * Auto-focusing of a freshly-opened modal element causes any child elements + * with the autofocus attribute to loose 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 (!element[0].querySelectorAll('[autofocus]').length) { + element[0].focus(); + } + }); + + scope.close = function (evt) { + var modal = $modalStack.getTop(); + if (modal && modal.value.backdrop && modal.value.backdrop != 'static' && (evt.target === evt.currentTarget)) { + evt.preventDefault(); + evt.stopPropagation(); + $modalStack.dismiss(modal.key, 'backdrop click'); + } + }; + } + }; + }]) + + .directive('modalTransclude', function () { + return { + link: function($scope, $element, $attrs, controller, $transclude) { + $transclude($scope.$parent, function(clone) { + $element.empty(); + $element.append(clone); + }); + } + }; + }) + + .factory('$modalStack', ['$transition', '$timeout', '$document', '$compile', '$rootScope', '$$stackedMap', + function ($transition, $timeout, $document, $compile, $rootScope, $$stackedMap) { + + var OPENED_MODAL_CLASS = 'modal-open'; + + var backdropDomEl, backdropScope; + var openedWindows = $$stackedMap.createNew(); + var $modalStack = {}; + + function backdropIndex() { + var topBackdropIndex = -1; + var opened = openedWindows.keys(); + for (var i = 0; i < opened.length; i++) { + if (openedWindows.get(opened[i]).value.backdrop) { + topBackdropIndex = i; + } + } + return topBackdropIndex; + } + + $rootScope.$watch(backdropIndex, function(newBackdropIndex){ + if (backdropScope) { + backdropScope.index = newBackdropIndex; + } + }); + + function removeModalWindow(modalInstance) { + + var body = $document.find('body').eq(0); + var modalWindow = openedWindows.get(modalInstance).value; + + //clean up the stack + openedWindows.remove(modalInstance); + + //remove window DOM element + removeAfterAnimate(modalWindow.modalDomEl, modalWindow.modalScope, 300, function() { + modalWindow.modalScope.$destroy(); + body.toggleClass(OPENED_MODAL_CLASS, openedWindows.length() > 0); + checkRemoveBackdrop(); + }); + } + + 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; + } + } + + function removeAfterAnimate(domEl, scope, emulateTime, done) { + // Closing animation + scope.animate = false; + + var transitionEndEventName = $transition.transitionEndEventName; + if (transitionEndEventName) { + // transition out + var timeout = $timeout(afterAnimating, emulateTime); + + domEl.bind(transitionEndEventName, function () { + $timeout.cancel(timeout); + afterAnimating(); + scope.$apply(); + }); + } else { + // Ensure this call is async + $timeout(afterAnimating); + } + + function afterAnimating() { + if (afterAnimating.done) { + return; + } + afterAnimating.done = true; + + domEl.remove(); + if (done) { + done(); + } + } + } + + $document.bind('keydown', function (evt) { + var modal; + + if (evt.which === 27) { + modal = openedWindows.top(); + if (modal && modal.value.keyboard) { + evt.preventDefault(); + $rootScope.$apply(function () { + $modalStack.dismiss(modal.key, 'escape key press'); + }); + } + } + }); + + $modalStack.open = function (modalInstance, modal) { + + openedWindows.add(modalInstance, { + deferred: modal.deferred, + modalScope: modal.scope, + backdrop: modal.backdrop, + keyboard: modal.keyboard + }); + + var body = $document.find('body').eq(0), + currBackdropIndex = backdropIndex(); + + if (currBackdropIndex >= 0 && !backdropDomEl) { + backdropScope = $rootScope.$new(true); + backdropScope.index = currBackdropIndex; + var angularBackgroundDomEl = angular.element('
'); + angularBackgroundDomEl.attr('backdrop-class', modal.backdropClass); + backdropDomEl = $compile(angularBackgroundDomEl)(backdropScope); + body.append(backdropDomEl); + } + + var angularDomEl = angular.element('
'); + angularDomEl.attr({ + 'template-url': modal.windowTemplateUrl, + 'window-class': modal.windowClass, + 'size': modal.size, + 'index': openedWindows.length() - 1, + '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); + if (modalWindow) { + modalWindow.value.deferred.resolve(result); + removeModalWindow(modalInstance); + } + }; + + $modalStack.dismiss = function (modalInstance, reason) { + var modalWindow = openedWindows.get(modalInstance); + if (modalWindow) { + modalWindow.value.deferred.reject(reason); + removeModalWindow(modalInstance); + } + }; + + $modalStack.dismissAll = function (reason) { + var topModal = this.getTop(); + while (topModal) { + this.dismiss(topModal.key, reason); + topModal = this.getTop(); + } + }; + + $modalStack.getTop = function () { + return openedWindows.top(); + }; + + return $modalStack; + }]) + + .provider('$modal', function () { + + var $modalProvider = { + options: { + backdrop: true, //can be also false or 'static' + keyboard: true + }, + $get: ['$injector', '$rootScope', '$q', '$http', '$templateCache', '$controller', '$modalStack', + function ($injector, $rootScope, $q, $http, $templateCache, $controller, $modalStack) { + + var $modal = {}; + + function getTemplatePromise(options) { + return options.template ? $q.when(options.template) : + $http.get(angular.isFunction(options.templateUrl) ? (options.templateUrl)() : options.templateUrl, + {cache: $templateCache}).then(function (result) { + return result.data; + }); + } + + function getResolvePromises(resolves) { + var promisesArr = []; + angular.forEach(resolves, function (value) { + if (angular.isFunction(value) || angular.isArray(value)) { + promisesArr.push($q.when($injector.invoke(value))); + } + }); + return promisesArr; + } + + $modal.open = function (modalOptions) { + + var modalResultDeferred = $q.defer(); + var modalOpenedDeferred = $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, + close: function (result) { + $modalStack.close(modalInstance, result); + }, + dismiss: function (reason) { + $modalStack.dismiss(modalInstance, reason); + } + }; + + //merge and clean up options + modalOptions = angular.extend({}, $modalProvider.options, modalOptions); + modalOptions.resolve = modalOptions.resolve || {}; + + //verify options + if (!modalOptions.template && !modalOptions.templateUrl) { + throw new Error('One of template or templateUrl options is required.'); + } + + var templateAndResolvePromise = + $q.all([getTemplatePromise(modalOptions)].concat(getResolvePromises(modalOptions.resolve))); + + + templateAndResolvePromise.then(function resolveSuccess(tplAndVars) { + + var modalScope = (modalOptions.scope || $rootScope).$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++]; + }); + + ctrlInstance = $controller(modalOptions.controller, ctrlLocals); + if (modalOptions.controllerAs) { + modalScope[modalOptions.controllerAs] = ctrlInstance; + } + } + + $modalStack.open(modalInstance, { + scope: modalScope, + deferred: modalResultDeferred, + content: tplAndVars[0], + backdrop: modalOptions.backdrop, + keyboard: modalOptions.keyboard, + backdropClass: modalOptions.backdropClass, + windowClass: modalOptions.windowClass, + windowTemplateUrl: modalOptions.windowTemplateUrl, + size: modalOptions.size + }); + + }, function resolveError(reason) { + modalResultDeferred.reject(reason); + }); + + templateAndResolvePromise.then(function () { + modalOpenedDeferred.resolve(true); + }, function () { + modalOpenedDeferred.reject(false); + }); + + 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) { + 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]; + + if (!ngModelCtrl) { + return; // do nothing if no ng-model + } + + // 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 + }; + } + + 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, number, number === currentPage); + pages.push(page); + } + + // Add links to move between page sets + if ( isMaxSized && ! rotate ) { + if ( startPage > 1 ) { + var previousPageSet = makePage(startPage - 1, '...', false); + pages.unshift(previousPageSet); + } + + 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); + } + }; + } + }; +}]) + +.constant('pagerConfig', { + itemsPerPage: 10, + previousText: '« Previous', + nextText: 'Next »', + align: true +}) + +.directive('pager', ['pagerConfig', function(pagerConfig) { + return { + restrict: 'EA', + scope: { + totalItems: '=', + previousText: '@', + nextText: '@' + }, + 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]; + + if (!ngModelCtrl) { + return; // do nothing if no ng-model + } + + scope.align = angular.isDefined(attrs.align) ? scope.$parent.$eval(attrs.align) : pagerConfig.align; + paginationCtrl.init(ngModelCtrl, pagerConfig); + } + }; +}]); + +/** + * The following features are still outstanding: animation as a + * 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' ] ) + +/** + * The $tooltip service creates tooltip- and popover-like directives as well as + * houses global options for them. + */ +.provider( '$tooltip', function () { + // The default options tooltip and popover. + var defaultOptions = { + placement: 'top', + animation: true, + popupDelay: 0 + }; + + // Default hide triggers for each show trigger + var triggerMap = { + 'mouseenter': 'mouseleave', + 'click': 'click', + 'focus': 'blur' + }; + + // The options specified to the provider globally. + var globalOptions = {}; + + /** + * `options({})` allows global configuration of all tooltips in the + * application. + * + * var app = angular.module( 'App', ['ui.bootstrap.tooltip'], function( $tooltipProvider ) { + * // place tooltips left instead of top by default + * $tooltipProvider.options( { placement: 'left' } ); + * }); + */ + 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' ); + */ + this.setTriggers = function setTriggers ( triggers ) { + angular.extend( triggerMap, triggers ); + }; + + /** + * This is a helper function for translating camel-case to snake-case. + */ + function snake_case(name){ + var regexp = /[A-Z]/g; + var separator = '-'; + return name.replace(regexp, function(letter, pos) { + return (pos ? separator : '') + letter.toLowerCase(); + }); + } + + /** + * 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 ); + + /** + * Returns an object of show and hide triggers. + * + * If a trigger is supplied, + * it is used to show the tooltip; otherwise, it will use the `trigger` + * option passed to the `$tooltipProvider.options` method; else it will + * default to the trigger supplied to this directive factory. + * + * The hide trigger is based on the show trigger. If the `trigger` option + * was passed to the `$tooltipProvider.options` method, it will use the + * mapped trigger from `triggerMap` or the passed trigger if the map is + * 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; + return { + show: show, + hide: hide + }; + } + + var directiveName = snake_case( type ); + + var startSym = $interpolate.startSymbol(); + var endSym = $interpolate.endSymbol(); + var template = + '
'+ + '
'; + + return { + restrict: 'EA', + scope: true, + compile: function (tElem, tAttrs) { + var tooltipLinker = $compile( template ); + + return function link ( scope, element, attrs ) { + var tooltip; + 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 positionTooltip = function () { + + var ttPosition = $position.positionElements(element, tooltip, scope.tt_placement, appendToBody); + ttPosition.top += 'px'; + ttPosition.left += 'px'; + + // Now set the calculated positioning. + tooltip.css( ttPosition ); + }; + + // By default, the tooltip is not open. + // TODO add ability to start tooltip opened + scope.tt_isOpen = false; + + function toggleTooltipBind () { + if ( ! scope.tt_isOpen ) { + showTooltipBind(); + } else { + hideTooltipBind(); + } + } + + // Show the tooltip with delay if specified, otherwise show it immediately + function showTooltipBind() { + if(hasEnableExp && !scope.$eval(attrs[prefix+'Enable'])) { + return; + } + if ( scope.tt_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();}); + } + } else { + show()(); + } + } + + function hideTooltipBind () { + scope.$apply(function () { + 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; + } + + // Don't show empty tooltips. + if ( ! scope.tt_content ) { + return angular.noop; + } + + createTooltip(); + + // Set the initial positioning. + tooltip.css({ top: 0, left: 0, display: 'block' }); + + // 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 ); + } + + 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; + } + + // Hide the tooltip popup element. + function hide() { + // 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); + } + } else { + removeTooltip(); + } + } + + function createTooltip() { + // There can only be one tooltip element per directive shown at once. + if (tooltip) { + removeTooltip(); + } + tooltip = tooltipLinker(scope, function () {}); + + // Get contents rendered into the tooltip + scope.$digest(); + } + + function removeTooltip() { + transitionTimeout = null; + if (tooltip) { + tooltip.remove(); + tooltip = null; + } + } + + /** + * Observe the relevant attributes. + */ + attrs.$observe( type, function ( val ) { + scope.tt_content = val; + + if (!val && scope.tt_isOpen ) { + hide(); + } + }); + + attrs.$observe( prefix+'Title', function ( val ) { + scope.tt_title = val; + }); + + attrs.$observe( prefix+'Placement', function ( val ) { + scope.tt_placement = angular.isDefined( val ) ? val : options.placement; + }); + + attrs.$observe( prefix+'PopupDelay', function ( val ) { + var delay = parseInt( val, 10 ); + scope.tt_popupDelay = ! isNaN(delay) ? delay : options.popupDelay; + }); + + var unregisterTriggers = function () { + element.unbind(triggers.show, showTooltipBind); + element.unbind(triggers.hide, hideTooltipBind); + }; + + attrs.$observe( prefix+'Trigger', function ( val ) { + unregisterTriggers(); + + triggers = getTriggers( val ); + + if ( triggers.show === triggers.hide ) { + element.bind( triggers.show, toggleTooltipBind ); + } else { + element.bind( triggers.show, showTooltipBind ); + element.bind( triggers.hide, hideTooltipBind ); + } + }); + + var animation = scope.$eval(attrs[prefix + 'Animation']); + scope.tt_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(); + } + }); + } + + // Make sure tooltip is destroyed and removed. + scope.$on('$destroy', function onDestroyTooltip() { + $timeout.cancel( transitionTimeout ); + $timeout.cancel( popupTimeout ); + unregisterTriggers(); + removeTooltip(); + }); + }; + } + }; + }; + }]; +}) + +.directive( 'tooltipPopup', function () { + return { + restrict: 'EA', + replace: true, + scope: { content: '@', placement: '@', animation: '&', isOpen: '&' }, + templateUrl: 'template/tooltip/tooltip-popup.html' + }; +}) + +.directive( 'tooltip', [ '$tooltip', function ( $tooltip ) { + return $tooltip( 'tooltip', 'tooltip', 'mouseenter' ); +}]) + +.directive( 'tooltipHtmlUnsafePopup', function () { + return { + restrict: 'EA', + replace: true, + scope: { content: '@', placement: '@', animation: '&', isOpen: '&' }, + templateUrl: 'template/tooltip/tooltip-html-unsafe-popup.html' + }; +}) + +.directive( 'tooltipHtmlUnsafe', [ '$tooltip', function ( $tooltip ) { + return $tooltip( 'tooltipHtmlUnsafe', 'tooltip', 'mouseenter' ); +}]); + +/** + * 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. + */ +angular.module( 'ui.bootstrap.popover', [ 'ui.bootstrap.tooltip' ] ) + +.directive( 'popoverPopup', function () { + return { + restrict: 'EA', + replace: true, + scope: { title: '@', content: '@', placement: '@', animation: '&', isOpen: '&' }, + templateUrl: 'template/popover/popover.html' + }; +}) + +.directive( 'popover', [ '$tooltip', function ( $tooltip ) { + return $tooltip( 'popover', 'popover', 'click' ); +}]); + +angular.module('ui.bootstrap.progressbar', []) + +.constant('progressConfig', { + 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; + + this.bars = []; + $scope.max = angular.isDefined($attrs.max) ? $scope.$parent.$eval($attrs.max) : progressConfig.max; + + this.addBar = function(bar, element) { + if ( !animate ) { + element.css({'transition': 'none'}); + } + + this.bars.push(bar); + + bar.$watch('value', function( value ) { + bar.percent = +(100 * value / $scope.max).toFixed(2); + }); + + bar.$on('$destroy', function() { + element = null; + self.removeBar(bar); + }); + }; + + this.removeBar = function(bar) { + this.bars.splice(this.bars.indexOf(bar), 1); + }; +}]) + +.directive('progress', function() { + return { + restrict: 'EA', + replace: true, + transclude: true, + controller: 'ProgressController', + require: 'progress', + scope: {}, + templateUrl: '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('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])); + } + }; +}); +angular.module('ui.bootstrap.rating', []) + +.constant('ratingConfig', { + max: 5, + stateOn: null, + stateOff: null +}) + +.controller('RatingController', ['$scope', '$attrs', 'ratingConfig', function($scope, $attrs, ratingConfig) { + var ngModelCtrl = { $setViewValue: angular.noop }; + + this.init = function(ngModelCtrl_) { + ngModelCtrl = ngModelCtrl_; + ngModelCtrl.$render = this.render; + + 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; + + 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]); + } + return states; + }; + + $scope.rate = function(value) { + if ( !$scope.readonly && value >= 0 && value <= $scope.range.length ) { + ngModelCtrl.$setViewValue(value); + ngModelCtrl.$render(); + } + }; + + $scope.enter = function(value) { + if ( !$scope.readonly ) { + $scope.value = value; + } + $scope.onHover({value: value}); + }; + + $scope.reset = function() { + $scope.value = ngModelCtrl.$viewValue; + $scope.onLeave(); + }; + + $scope.onKeydown = function(evt) { + if (/(37|38|39|40)/.test(evt.which)) { + evt.preventDefault(); + evt.stopPropagation(); + $scope.rate( $scope.value + (evt.which === 38 || evt.which === 39 ? 1 : -1) ); + } + }; + + this.render = function() { + $scope.value = ngModelCtrl.$viewValue; + }; +}]) + +.directive('rating', function() { + return { + restrict: 'EA', + require: ['rating', 'ngModel'], + scope: { + readonly: '=?', + onHover: '&', + onLeave: '&' + }, + controller: 'RatingController', + templateUrl: 'template/rating/rating.html', + replace: true, + link: function(scope, element, attrs, ctrls) { + var ratingCtrl = ctrls[0], ngModelCtrl = ctrls[1]; + + if ( 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) { + var ctrl = this, + tabs = ctrl.tabs = $scope.tabs = []; + + ctrl.select = function(selectedTab) { + angular.forEach(tabs, function(tab) { + if (tab.active && tab !== selectedTab) { + tab.active = false; + tab.onDeselect(); + } + }); + selectedTab.active = true; + selectedTab.onSelect(); + }; + + 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.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]); + } + tabs.splice(index, 1); + }; +}]) + +/** + * @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() { + return { + restrict: 'EA', + transclude: true, + replace: true, + scope: { + type: '@' + }, + controller: 'TabsetController', + templateUrl: '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; + } + }; +}) + +/** + * @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) { + return { + require: '^tabset', + restrict: 'EA', + replace: true, + templateUrl: 'template/tabs/tab.html', + transclude: true, + scope: { + active: '=?', + heading: '@', + onSelect: '&select', //This callback is called in contentHeadingTransclude + //once it inserts the tab's content into the dom + onDeselect: '&deselect' + }, + 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); + } + }); + + scope.disabled = false; + if ( attrs.disabled ) { + scope.$parent.$watch($parse(attrs.disabled), function(value) { + scope.disabled = !! value; + }); + } + + scope.select = function() { + if ( !scope.disabled ) { + scope.active = true; + } + }; + + 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() { + return { + restrict: 'A', + require: '^tab', + link: function(scope, elm, attrs, tabCtrl) { + scope.$watch('headingElement', function updateHeadingElement(heading) { + if (heading) { + elm.html(''); + elm.append(heading); + } + }); + } + }; +}]) + +.directive('tabContentTransclude', function() { + return { + restrict: 'A', + require: '^tabset', + link: function(scope, elm, attrs) { + var tab = scope.$eval(attrs.tabContentTransclude); + + //Now our tab is ready to be transcluded: both the tab heading area + //and the tab content area are loaded. Transclude 'em both. + tab.$transcludeFn(tab.$parent, function(contents) { + angular.forEach(contents, function(node) { + if (isTabHeading(node)) { + //Let tabHeadingTransclude know. + tab.headingElement = node; + } else { + elm.append(node); + } + }); + }); + } + }; + 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' + ); + } +}) + +; + +angular.module('ui.bootstrap.timepicker', []) + +.constant('timepickerConfig', { + hourStep: 1, + minuteStep: 1, + showMeridian: true, + meridians: null, + readonlyInput: false, + mousewheel: true +}) + +.controller('TimepickerController', ['$scope', '$attrs', '$parse', '$log', '$locale', 'timepickerConfig', function($scope, $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; + + this.init = function( ngModelCtrl_, inputs ) { + ngModelCtrl = ngModelCtrl_; + ngModelCtrl.$render = this.render; + + var hoursInputEl = inputs.eq(0), + minutesInputEl = inputs.eq(1); + + var mousewheel = angular.isDefined($attrs.mousewheel) ? $scope.$parent.$eval($attrs.mousewheel) : timepickerConfig.mousewheel; + if ( mousewheel ) { + this.setupMousewheelEvents( hoursInputEl, minutesInputEl ); + } + + $scope.readonlyInput = angular.isDefined($attrs.readonlyInput) ? $scope.$parent.$eval($attrs.readonlyInput) : timepickerConfig.readonlyInput; + this.setupInputEvents( hoursInputEl, minutesInputEl ); + }; + + var hourStep = timepickerConfig.hourStep; + if ($attrs.hourStep) { + $scope.$parent.$watch($parse($attrs.hourStep), function(value) { + hourStep = parseInt(value, 10); + }); + } + + var minuteStep = timepickerConfig.minuteStep; + if ($attrs.minuteStep) { + $scope.$parent.$watch($parse($attrs.minuteStep), function(value) { + minuteStep = parseInt(value, 10); + }); + } + + // 12H / 24H mode + $scope.showMeridian = timepickerConfig.showMeridian; + if ($attrs.showMeridian) { + $scope.$parent.$watch($parse($attrs.showMeridian), function(value) { + $scope.showMeridian = !!value; + + if ( ngModelCtrl.$error.time ) { + // Evaluate from template + var hours = getHoursFromTemplate(), minutes = getMinutesFromTemplate(); + 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 ) { + return undefined; + } + + if ( $scope.showMeridian ) { + if ( hours === 12 ) { + hours = 0; + } + if ( $scope.meridian === meridians[1] ) { + hours = hours + 12; + } + } + return hours; + } + + function getMinutesFromTemplate() { + var minutes = parseInt($scope.minutes, 10); + return ( minutes >= 0 && minutes < 60 ) ? minutes : undefined; + } + + function pad( value ) { + return ( angular.isDefined(value) && value.toString().length < 2 ) ? '0' + value : value; + } + + // Respond on mousewheel spin + this.setupMousewheelEvents = function( hoursInputEl, minutesInputEl ) { + 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); + }; + + hoursInputEl.bind('mousewheel wheel', function(e) { + $scope.$apply( (isScrollingUp(e)) ? $scope.incrementHours() : $scope.decrementHours() ); + e.preventDefault(); + }); + + minutesInputEl.bind('mousewheel wheel', function(e) { + $scope.$apply( (isScrollingUp(e)) ? $scope.incrementMinutes() : $scope.decrementMinutes() ); + e.preventDefault(); + }); + + }; + + this.setupInputEvents = function( hoursInputEl, minutesInputEl ) { + if ( $scope.readonlyInput ) { + $scope.updateHours = angular.noop; + $scope.updateMinutes = angular.noop; + return; + } + + var invalidate = function(invalidHours, invalidMinutes) { + ngModelCtrl.$setViewValue( null ); + ngModelCtrl.$setValidity('time', false); + if (angular.isDefined(invalidHours)) { + $scope.invalidHours = invalidHours; + } + if (angular.isDefined(invalidMinutes)) { + $scope.invalidMinutes = invalidMinutes; + } + }; + + $scope.updateHours = function() { + var hours = getHoursFromTemplate(); + + if ( angular.isDefined(hours) ) { + selected.setHours( hours ); + refresh( 'h' ); + } else { + invalidate(true); + } + }; + + hoursInputEl.bind('blur', function(e) { + if ( !$scope.invalidHours && $scope.hours < 10) { + $scope.$apply( function() { + $scope.hours = pad( $scope.hours ); + }); + } + }); + + $scope.updateMinutes = function() { + var minutes = getMinutesFromTemplate(); + + if ( angular.isDefined(minutes) ) { + selected.setMinutes( minutes ); + refresh( 'm' ); + } else { + invalidate(undefined, true); + } + }; + + minutesInputEl.bind('blur', function(e) { + if ( !$scope.invalidMinutes && $scope.minutes < 10 ) { + $scope.$apply( function() { + $scope.minutes = pad( $scope.minutes ); + }); + } + }); + + }; + + this.render = function() { + var date = ngModelCtrl.$modelValue ? new Date( ngModelCtrl.$modelValue ) : null; + + 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 ) { + selected = date; + } + makeValid(); + updateTemplate(); + } + }; + + // Call internally when we know that model is valid. + function refresh( keyboardChange ) { + makeValid(); + ngModelCtrl.$setViewValue( new Date(selected) ); + updateTemplate( keyboardChange ); + } + + function makeValid() { + ngModelCtrl.$setValidity('time', true); + $scope.invalidHours = false; + $scope.invalidMinutes = false; + } + + function updateTemplate( keyboardChange ) { + var hours = selected.getHours(), minutes = selected.getMinutes(); + + if ( $scope.showMeridian ) { + hours = ( hours === 0 || hours === 12 ) ? 12 : hours % 12; // Convert 24 to 12 hour system + } + + $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() ); + refresh(); + } + + $scope.incrementHours = function() { + addMinutes( hourStep * 60 ); + }; + $scope.decrementHours = function() { + addMinutes( - hourStep * 60 ); + }; + $scope.incrementMinutes = function() { + addMinutes( minuteStep ); + }; + $scope.decrementMinutes = function() { + addMinutes( - minuteStep ); + }; + $scope.toggleMeridian = function() { + addMinutes( 12 * 60 * (( selected.getHours() < 12 ) ? 1 : -1) ); + }; +}]) + +.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') ); + } + } + }; +}); + +angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap.bindHtml']) + +/** + * 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\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]) + }; + } + }; +}]) + + .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)); + } + }); + + var getMatchesAsync = function(inputValue) { + + var locals = {$viewValue: inputValue}; + isLoadingSetter(originalScope, true); + $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.length > 0) { + + scope.activeIdx = 0; + scope.matches.length = 0; + + //transform labels + for(var i=0; i= minSearch) { + if (waitTime > 0) { + 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; + } + } + }); + + modelCtrl.$formatters.push(function (modelValue) { + + var candidateViewValue, emptyViewValue; + var locals = {}; + + if (inputFormatter) { + + 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(); + + 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(); + } + }); + + 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 () { + 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}); + }; + } + }; + }) + + .directive('typeaheadMatch', ['$http', '$templateCache', '$compile', '$parse', function ($http, $templateCache, $compile, $parse) { + return { + restrict:'EA', + scope:{ + index:'=', + match:'=', + query:'=' + }, + 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)); + }); + } + }; + }]) + + .filter('typeaheadHighlight', function() { + + function escapeRegexp(queryToEscape) { + return queryToEscape.replace(/([.?*+^$[\]\\(){}|-])/g, '\\$1'); + } + + return function(matchItem, query) { + return query ? ('' + matchItem).replace(new RegExp(escapeRegexp(query), 'gi'), '$&') : matchItem; + }; + }); + +angular.module("template/accordion/accordion-group.html", []).run(["$templateCache", function($templateCache) { + $templateCache.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($templateCache) { + $templateCache.put("template/accordion/accordion.html", + "
"); +}]); + +angular.module("template/alert/alert.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("template/alert/alert.html", + "
\n" + + " \n" + + "
\n" + + "
\n" + + ""); +}]); + +angular.module("template/carousel/carousel.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("template/carousel/carousel.html", + "
\n" + + "
    1\">\n" + + "
  1. \n" + + "
\n" + + "
\n" + + " 1\">\n" + + " 1\">\n" + + "
\n" + + ""); +}]); + +angular.module("template/carousel/slide.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("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("template/datepicker/day.html", []).run(["$templateCache", function($templateCache) { + $templateCache.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($templateCache) { + $templateCache.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($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" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + "
\n" + + " \n" + + "
\n" + + ""); +}]); + +angular.module("template/modal/backdrop.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("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" + + "
\n" + + ""); +}]); + +angular.module("template/tooltip/tooltip-popup.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("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" + + "
\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("template/progressbar/progress.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("template/progressbar/progress.html", + "
"); +}]); + +angular.module("template/progressbar/progressbar.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("template/progressbar/progressbar.html", + "
\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("template/tabs/tab.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("template/tabs/tab.html", + "
  • \n" + + " {{heading}}\n" + + "
  • \n" + + ""); +}]); + +angular.module("template/tabs/tabset.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("template/tabs/tabset.html", + "
    \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" + + "
       
      \n" + + ""); +}]); + +angular.module("template/typeahead/typeahead-match.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("template/typeahead/typeahead-match.html", + ""); +}]); + +angular.module("template/typeahead/typeahead-popup.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("template/typeahead/typeahead-popup.html", + "
        \n" + + "
      • \n" + + "
        \n" + + "
      • \n" + + "
      \n" + + ""); +}]); diff --git a/src/components/angular-bootstrap/ui-bootstrap-tpls.min.js b/src/components/angular-bootstrap/ui-bootstrap-tpls.min.js new file mode 100644 index 00000000..10e140e8 --- /dev/null +++ b/src/components/angular-bootstrap/ui-bootstrap-tpls.min.js @@ -0,0 +1,10 @@ +/* + * angular-ui-bootstrap + * http://angular-ui.github.io/bootstrap/ + + * Version: 0.11.2 - 2014-09-26 + * 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){var c=[],d=a.split("");return angular.forEach(e,function(b,e){var f=a.indexOf(e);if(f>-1){a=a.split(""),d[f]="("+b.regex+")",a[f]="$";for(var g=f+1,h=f+e.length;h>g;g++)d[g]="",a[g]="$";a=a.join(""),c.push({index:f,apply:b.apply})}}),{regex:new RegExp("^"+d.join("")+"$"),map:b(c,"index")}}function d(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 e={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.parse=function(b,e){if(!angular.isString(b)||!e)return b;e=a.DATETIME_FORMATS[e]||e,this.parsers[e]||(this.parsers[e]=c(e));var f=this.parsers[e],g=f.regex,h=f.map,i=b.match(g);if(i&&i.length){for(var j,k={year:1900,month:0,date:1,hours:0},l=1,m=i.length;m>l;l++){var n=h[l-1];n.apply&&n.apply.call(k,i[l])}return d(k.year,k.month,k.date)&&(j=new Date(k.year,k.month,k.date,k.hours)),j}}}]),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)}),h.watchData={},angular.forEach(["minDate","maxDate","datepickerMode"],function(a){if(j[a]){var c=b(j[a]);if(h.$parent.$watch(c,function(b){h.watchData[a]=b}),r.attr(l(a),"watchData."+a),"datepickerMode"===a){var d=c.assign;h.$watch("watchData."+a,function(a,b){a!==b&&d(h.$parent,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);q.remove(),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){var c=b.getToggleElement();a&&c&&c[0].contains(a.target)||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.getToggleElement=function(){return h.toggleElement},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)}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();if(h>=0&&!k){l=e.$new(!0),l.index=h;var i=angular.element("
      ");i.attr("backdrop-class",b.backdropClass),k=d(i)(l),f.append(k)}var j=angular.element("
      ");j.attr({"template-url":b.windowTemplateUrl,"window-class":b.windowClass,size:b.size,index:n.length()-1,animate:"animate"}).html(b.content);var o=d(j)(b.scope);n.top().value.modalDomEl=o,f.append(o),f.addClass(m)},o.close=function(a,b){var c=n.get(a);c&&(c.value.deferred.resolve(b),h(a))},o.dismiss=function(a,b){var c=n.get(a);c&&(c.value.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(angular.isFunction(a.templateUrl)?a.templateUrl():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),b.controllerAs&&(d[b.controllerAs]=f)),h.open(k,{scope:d,deferred:e,content:a[0],backdrop:b.backdrop,keyboard:b.keyboard,backdropClass:b.backdropClass,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\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])}}}}]).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?(E(),D(a)):B(a):(q(i,!1),E(),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 F=function(a){j[0]!==a.target&&(z(),w.$digest())};e.bind("click",F),i.$on("$destroy",function(){e.unbind("click",F)});var G=a(y)(w);t?e.find("body").append(G):j.after(G)}}}]).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.html",[]).run(["$templateCache",function(a){a.put("template/tabs/tabset.html",'
      \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",'\n')}]); \ No newline at end of file diff --git a/src/components/angular-bootstrap/ui-bootstrap.js b/src/components/angular-bootstrap/ui-bootstrap.js new file mode 100644 index 00000000..9d369325 --- /dev/null +++ b/src/components/angular-bootstrap/ui-bootstrap.js @@ -0,0 +1,3857 @@ +/* + * angular-ui-bootstrap + * http://angular-ui.github.io/bootstrap/ + + * Version: 0.11.2 - 2014-09-26 + * 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) { + + return { + link: function (scope, element, attrs) { + + 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; + } + } + } + + function expand() { + if (initialAnimSkip) { + initialAnimSkip = false; + expandDone(); + } else { + element.removeClass('collapse').addClass('collapsing'); + doTransition({ height: element[0].scrollHeight + 'px' }).then(expandDone); + } + } + + function expandDone() { + element.removeClass('collapsing'); + element.addClass('collapse in'); + element.css({height: 'auto'}); + } + + 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); + } + } + + function collapseDone() { + element.removeClass('collapsing'); + element.addClass('collapse'); + } + + scope.$watch(attrs.collapse, function (shouldCollapse) { + if (shouldCollapse) { + collapse(); + } else { + expand(); + } + }); + } + }; + }]); + +angular.module('ui.bootstrap.accordion', ['ui.bootstrap.collapse']) + +.constant('accordionConfig', { + closeOthers: true +}) + +.controller('AccordionController', ['$scope', '$attrs', 'accordionConfig', 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 ) { + group.isOpen = false; + } + }); + } + }; + + // This is called from the accordion-group directive to add itself to the accordion + this.addGroup = function(groupScope) { + var that = this; + this.groups.push(groupScope); + + groupScope.$on('$destroy', function (event) { + that.removeGroup(groupScope); + }); + }; + + // 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 ) { + 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 () { + return { + restrict:'EA', + controller:'AccordionController', + transclude: true, + replace: false, + templateUrl: 'template/accordion/accordion.html' + }; +}) + +// The accordion-group directive indicates a block of html that will expand and collapse in an accordion +.directive('accordionGroup', 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 + replace: true, // The element containing the directive will be replaced with the template + templateUrl:'template/accordion/accordion-group.html', + scope: { + heading: '@', // Interpolate the heading attribute onto this scope + isOpen: '=?', + isDisabled: '=?' + }, + controller: function() { + this.setHeading = function(element) { + this.heading = element; + }; + }, + link: function(scope, element, attrs, accordionCtrl) { + accordionCtrl.addGroup(scope); + + scope.$watch('isOpen', function(value) { + if ( value ) { + accordionCtrl.closeOthers(scope); + } + }); + + scope.toggleOpen = function() { + if ( !scope.isDisabled ) { + scope.isOpen = !scope.isOpen; + } + }; + } + }; +}) + +// Use accordion-heading below an accordion-group to provide a heading containing HTML +// +// Heading containing HTML - +// +.directive('accordionHeading', 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) { + // 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() {})); + } + }; +}) + +// 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() { + 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); + } + }); + } + }; +}); + +angular.module('ui.bootstrap.alert', []) + +.controller('AlertController', ['$scope', '$attrs', function ($scope, $attrs) { + $scope.closeable = 'close' in $attrs; +}]) + +.directive('alert', function () { + return { + restrict:'EA', + controller:'AlertController', + templateUrl:'template/alert/alert.html', + transclude:true, + replace:true, + scope: { + type: '@', + close: '&' + } + }; +}); + +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', { + activeClass: 'active', + toggleEvent: 'click' +}) + +.controller('ButtonsController', ['buttonConfig', function(buttonConfig) { + this.activeClass = buttonConfig.activeClass || 'active'; + this.toggleEvent = buttonConfig.toggleEvent || 'click'; +}]) + +.directive('btnRadio', function () { + return { + require: ['btnRadio', 'ngModel'], + controller: 'ButtonsController', + link: function (scope, element, attrs, ctrls) { + var buttonsCtrl = ctrls[0], ngModelCtrl = ctrls[1]; + + //model -> UI + ngModelCtrl.$render = function () { + element.toggleClass(buttonsCtrl.activeClass, angular.equals(ngModelCtrl.$modelValue, scope.$eval(attrs.btnRadio))); + }; + + //ui->model + element.bind(buttonsCtrl.toggleEvent, function () { + var isActive = element.hasClass(buttonsCtrl.activeClass); + + if (!isActive || angular.isDefined(attrs.uncheckable)) { + scope.$apply(function () { + ngModelCtrl.$setViewValue(isActive ? null : scope.$eval(attrs.btnRadio)); + ngModelCtrl.$render(); + }); + } + }); + } + }; +}) + +.directive('btnCheckbox', function () { + return { + require: ['btnCheckbox', 'ngModel'], + controller: 'ButtonsController', + link: function (scope, element, attrs, ctrls) { + var buttonsCtrl = ctrls[0], ngModelCtrl = ctrls[1]; + + function getTrueValue() { + return getCheckboxValue(attrs.btnCheckboxTrue, true); + } + + function getFalseValue() { + return getCheckboxValue(attrs.btnCheckboxFalse, false); + } + + function getCheckboxValue(attributeValue, defaultValue) { + var val = scope.$eval(attributeValue); + return angular.isDefined(val) ? val : defaultValue; + } + + //model -> UI + ngModelCtrl.$render = function () { + element.toggleClass(buttonsCtrl.activeClass, angular.equals(ngModelCtrl.$modelValue, getTrueValue())); + }; + + //ui->model + element.bind(buttonsCtrl.toggleEvent, function () { + scope.$apply(function () { + ngModelCtrl.$setViewValue(element.hasClass(buttonsCtrl.activeClass) ? getFalseValue() : getTrueValue()); + ngModelCtrl.$render(); + }); + }); + } + }; +}); + +/** +* @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) { + var self = this, + slides = self.slides = $scope.slides = [], + currentIndex = -1, + currentTimeout, isPlaying; + self.currentSlide = null; + + var destroyed = false; + /* direction: "prev" or "next" */ + self.select = $scope.select = function(nextSlide, direction) { + var nextIndex = slides.indexOf(nextSlide); + //Decide direction if it's not given + if (direction === undefined) { + direction = nextIndex > currentIndex ? '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; + } + }; + $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.isActive = function(slide) { + return self.currentSlide === slide; + }; + + $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.pause = function() { + if (!$scope.noPause) { + isPlaying = false; + resetTimer(); + } + }; + + 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; + } + }; + + 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 { + self.select(slides[index]); + } + } else if (currentIndex > index) { + currentIndex--; + } + }; + +}]) + +/** + * @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() { + return { + restrict: 'EA', + transclude: true, + replace: true, + controller: 'CarouselController', + require: 'carousel', + templateUrl: 'template/carousel/carousel.html', + scope: { + interval: '=', + noTransition: '=', + noPause: '=' + } + }; +}]) + +/** + * @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() { + return { + require: '^carousel', + restrict: 'EA', + transclude: true, + replace: true, + templateUrl: 'template/carousel/slide.html', + scope: { + active: '=?' + }, + link: function (scope, element, attrs, carouselCtrl) { + carouselCtrl.addSlide(scope, element); + //when the scope is destroyed then remove the slide from the current slides array + scope.$on('$destroy', function() { + carouselCtrl.removeSlide(scope); + }); + + scope.$watch('active', function(active) { + if (active) { + carouselCtrl.select(scope); + } + }); + } + }; +}); + +angular.module('ui.bootstrap.dateparser', []) + +.service('dateParser', ['$locale', 'orderByFilter', function($locale, orderByFilter) { + + this.parsers = {}; + + 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('|') + } + }; + + function createParser(format) { + var map = [], regex = format.split(''); + + angular.forEach(formatCodeToRegex, function(data, code) { + var index = format.indexOf(code); + + 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++) { + regex[i] = ''; + format[i] = '$'; + } + format = format.join(''); + + map.push({ index: index, apply: data.apply }); + } + }); + + return { + regex: new RegExp('^' + regex.join('') + '$'), + map: orderByFilter(map, 'index') + }; + } + + this.parse = function(input, format) { + if ( !angular.isString(input) || !format ) { + return input; + } + + format = $locale.DATETIME_FORMATS[format] || format; + + if ( !this.parsers[format] ) { + this.parsers[format] = createParser(format); + } + + var parser = this.parsers[format], + regex = parser.regex, + map = parser.map, + results = input.match(regex); + + 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.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); + } + + return dt; + } + }; + + // 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 ( month === 3 || month === 5 || month === 8 || month === 10) { + return date < 31; + } + + return true; + } +}]); + +angular.module('ui.bootstrap.position', []) + +/** + * 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) { + + function getStyle(el, cssprop) { + if (el.currentStyle) { //IE + return el.currentStyle[cssprop]; + } else if ($window.getComputedStyle) { + return $window.getComputedStyle(el)[cssprop]; + } + // 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; + } + + 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; + } + }; + }]); + +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: true, + startingDay: 0, + yearRange: 20, + minDate: null, + maxDate: null +}) + +.controller('DatepickerController', ['$scope', '$attrs', '$parse', '$interpolate', '$timeout', '$log', 'dateFilter', 'datepickerConfig', function($scope, $attrs, $parse, $interpolate, $timeout, $log, dateFilter, datepickerConfig) { + var self = this, + ngModelCtrl = { $setViewValue: angular.noop }; // nullModelCtrl; + + // 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]; + }); + + // Watchable date 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; + } + }); + + $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.isActive = function(dateObject) { + if (self.compare(dateObject.date, self.activeDate) === 0) { + $scope.activeDateId = dateObject.uid; + return true; + } + return false; + }; + + this.init = function( ngModelCtrl_ ) { + ngModelCtrl = ngModelCtrl_; + + ngModelCtrl.$render = function() { + self.render(); + }; + }; + + this.render = function() { + if ( ngModelCtrl.$modelValue ) { + var date = new Date( ngModelCtrl.$modelValue ), + 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.'); + } + ngModelCtrl.$setValidity('date', isValid); + } + this.refreshView(); + }; + + this.refreshView = function() { + if ( this.element ) { + this._refreshView(); + + var date = ngModelCtrl.$modelValue ? new Date(ngModelCtrl.$modelValue) : null; + ngModelCtrl.$setValidity('date-disabled', !date || (this.element && !this.isDisabled(date))); + } + }; + + this.createDateObject = function(date, format) { + var model = ngModelCtrl.$modelValue ? new Date(ngModelCtrl.$modelValue) : null; + return { + date: date, + label: dateFilter(date, format), + selected: model && this.compare(date, model) === 0, + disabled: this.isDisabled(date), + current: this.compare(date, new Date()) === 0 + }; + }; + + 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}))); + }; + + // Split array into smaller arrays + this.split = function(arr, size) { + var arrays = []; + while (arr.length > 0) { + arrays.push(arr.splice(0, size)); + } + 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 ); + ngModelCtrl.$render(); + } else { + self.activeDate = date; + $scope.datepickerMode = self.modes[ self.modes.indexOf( $scope.datepickerMode ) - 1 ]; + } + }; + + $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 ) { + direction = 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 ]; + }; + + // 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' }; + + var focusElement = function() { + $timeout(function() { + self.element[0].focus(); + }, 0 , false); + }; + + // Listen for focus requests from popup directive + $scope.$on('datepicker.focus', focusElement); + + $scope.keydown = function( evt ) { + var key = $scope.keys[evt.which]; + + if ( !key || evt.shiftKey || evt.altKey ) { + return; + } + + evt.preventDefault(); + evt.stopPropagation(); + + if (key === 'enter' || key === 'space') { + 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(); + } + }; +}]) + +.directive( 'datepicker', function () { + return { + restrict: 'EA', + replace: true, + templateUrl: 'template/datepicker/datepicker.html', + scope: { + datepickerMode: '=?', + dateDisabled: '&' + }, + require: ['datepicker', '?^ngModel'], + controller: 'DatepickerController', + link: function(scope, element, attrs, ctrls) { + var datepickerCtrl = ctrls[0], ngModelCtrl = ctrls[1]; + + if ( ngModelCtrl ) { + datepickerCtrl.init( ngModelCtrl ); + } + } + }; +}) + +.directive('daypicker', ['dateFilter', function (dateFilter) { + 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: '&' + }, + 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; + + 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 ); + }); + } + + scope.watchData = {}; + angular.forEach(['minDate', 'maxDate', 'datepickerMode'], function( key ) { + if ( attrs[key] ) { + var getAttribute = $parse(attrs[key]); + scope.$parent.$watch(getAttribute, function(value){ + scope.watchData[key] = value; + }); + datepickerEl.attr(cameltoDash(key), 'watchData.' + key); + + // Propagate changes from datepicker to outside + if ( key === 'datepickerMode' ) { + var setAttribute = getAttribute.assign; + scope.$watch('watchData.' + key, function(value, oldvalue) { + if ( value !== oldvalue ) { + setAttribute(scope.$parent, value); + } + }); + } + } + }); + 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); + // 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() { + $popup.remove(); + element.unbind('keydown', keydown); + $document.unbind('click', documentClickBind); + }); + } + }; +}]) + +.directive('datepickerPopupWrap', 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(); + }); + } + }; +}); + +angular.module('ui.bootstrap.dropdown', []) + +.constant('dropdownConfig', { + openClass: 'open' +}) + +.service('dropdownService', ['$document', function($document) { + var openScope = null; + + this.open = function( dropdownScope ) { + if ( !openScope ) { + $document.bind('click', closeDropdown); + $document.bind('keydown', escapeKeyBind); + } + + if ( openScope && openScope !== dropdownScope ) { + openScope.isOpen = false; + } + + openScope = dropdownScope; + }; + + this.close = function( dropdownScope ) { + if ( openScope === dropdownScope ) { + openScope = null; + $document.unbind('click', closeDropdown); + $document.unbind('keydown', escapeKeyBind); + } + }; + + var closeDropdown = function( evt ) { + var toggleElement = openScope.getToggleElement(); + if ( evt && toggleElement && toggleElement[0].contains(evt.target) ) { + return; + } + + openScope.$apply(function() { + openScope.isOpen = false; + }); + }; + + var escapeKeyBind = function( evt ) { + if ( evt.which === 27 ) { + openScope.focusToggleElement(); + closeDropdown(); + } + }; +}]) + +.controller('DropdownController', ['$scope', '$attrs', '$parse', 'dropdownConfig', 'dropdownService', '$animate', function($scope, $attrs, $parse, dropdownConfig, dropdownService, $animate) { + 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; + + this.init = function( element ) { + self.$element = element; + + if ( $attrs.isOpen ) { + getIsOpen = $parse($attrs.isOpen); + setIsOpen = getIsOpen.assign; + + $scope.$watch(getIsOpen, function(value) { + scope.isOpen = !!value; + }); + } + }; + + this.toggle = function( open ) { + return scope.isOpen = arguments.length ? !!open : !scope.isOpen; + }; + + // Allow other directives to watch status + this.isOpen = function() { + return scope.isOpen; + }; + + scope.getToggleElement = function() { + return self.toggleElement; + }; + + scope.focusToggleElement = function() { + if ( self.toggleElement ) { + self.toggleElement[0].focus(); + } + }; + + scope.$watch('isOpen', function( isOpen, wasOpen ) { + $animate[isOpen ? 'addClass' : 'removeClass'](self.$element, openClass); + + if ( isOpen ) { + scope.focusToggleElement(); + dropdownService.open( scope ); + } else { + dropdownService.close( scope ); + } + + setIsOpen($scope, isOpen); + if (angular.isDefined(isOpen) && isOpen !== wasOpen) { + toggleInvoker($scope, { open: !!isOpen }); + } + }); + + $scope.$on('$locationChangeSuccess', function() { + scope.isOpen = false; + }); + + $scope.$on('$destroy', function() { + scope.$destroy(); + }); +}]) + +.directive('dropdown', function() { + return { + restrict: 'CA', + controller: 'DropdownController', + link: function(scope, element, attrs, dropdownCtrl) { + dropdownCtrl.init( element ); + } + }; +}) + +.directive('dropdownToggle', function() { + return { + restrict: 'CA', + require: '?^dropdown', + link: function(scope, element, attrs, dropdownCtrl) { + if ( !dropdownCtrl ) { + return; + } + + dropdownCtrl.toggleElement = element; + + var toggleDropdown = function(event) { + event.preventDefault(); + + if ( !element.hasClass('disabled') && !attrs.disabled ) { + scope.$apply(function() { + dropdownCtrl.toggle(); + }); + } + }; + + element.bind('click', toggleDropdown); + + // WAI-ARIA + element.attr({ 'aria-haspopup': true, 'aria-expanded': false }); + scope.$watch(dropdownCtrl.isOpen, function( isOpen ) { + element.attr('aria-expanded', !!isOpen); + }); + + scope.$on('$destroy', function() { + element.unbind('click', toggleDropdown); + }); + } + }; +}); + +angular.module('ui.bootstrap.modal', ['ui.bootstrap.transition']) + +/** + * A helper, internal data structure that acts as a map but also allows getting / removing + * elements in the LIFO order + */ + .factory('$$stackedMap', function () { + return { + createNew: function () { + var stack = []; + + return { + add: function (key, value) { + stack.push({ + key: key, + value: value + }); + }, + get: function (key) { + for (var i = 0; i < stack.length; i++) { + if (key == stack[i].key) { + return stack[i]; + } + } + }, + keys: function() { + var keys = []; + for (var i = 0; i < stack.length; i++) { + keys.push(stack[i].key); + } + return keys; + }, + top: function () { + return stack[stack.length - 1]; + }, + remove: function (key) { + var idx = -1; + for (var i = 0; i < stack.length; i++) { + if (key == stack[i].key) { + idx = i; + break; + } + } + return stack.splice(idx, 1)[0]; + }, + removeTop: function () { + return stack.splice(stack.length - 1, 1)[0]; + }, + length: function () { + return stack.length; + } + }; + } + }; + }) + +/** + * A helper directive for the $modal service. It creates a backdrop element. + */ + .directive('modalBackdrop', ['$timeout', function ($timeout) { + return { + restrict: 'EA', + replace: true, + templateUrl: 'template/modal/backdrop.html', + link: function (scope, element, attrs) { + scope.backdropClass = attrs.backdropClass || ''; + + scope.animate = false; + + //trigger CSS transitions + $timeout(function () { + scope.animate = true; + }); + } + }; + }]) + + .directive('modalWindow', ['$modalStack', '$timeout', function ($modalStack, $timeout) { + return { + restrict: 'EA', + scope: { + index: '@', + animate: '=' + }, + replace: true, + transclude: true, + templateUrl: function(tElement, tAttrs) { + return tAttrs.templateUrl || 'template/modal/window.html'; + }, + link: function (scope, element, attrs) { + element.addClass(attrs.windowClass || ''); + scope.size = attrs.size; + + $timeout(function () { + // trigger CSS transitions + scope.animate = true; + + /** + * Auto-focusing of a freshly-opened modal element causes any child elements + * with the autofocus attribute to loose 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 (!element[0].querySelectorAll('[autofocus]').length) { + element[0].focus(); + } + }); + + scope.close = function (evt) { + var modal = $modalStack.getTop(); + if (modal && modal.value.backdrop && modal.value.backdrop != 'static' && (evt.target === evt.currentTarget)) { + evt.preventDefault(); + evt.stopPropagation(); + $modalStack.dismiss(modal.key, 'backdrop click'); + } + }; + } + }; + }]) + + .directive('modalTransclude', function () { + return { + link: function($scope, $element, $attrs, controller, $transclude) { + $transclude($scope.$parent, function(clone) { + $element.empty(); + $element.append(clone); + }); + } + }; + }) + + .factory('$modalStack', ['$transition', '$timeout', '$document', '$compile', '$rootScope', '$$stackedMap', + function ($transition, $timeout, $document, $compile, $rootScope, $$stackedMap) { + + var OPENED_MODAL_CLASS = 'modal-open'; + + var backdropDomEl, backdropScope; + var openedWindows = $$stackedMap.createNew(); + var $modalStack = {}; + + function backdropIndex() { + var topBackdropIndex = -1; + var opened = openedWindows.keys(); + for (var i = 0; i < opened.length; i++) { + if (openedWindows.get(opened[i]).value.backdrop) { + topBackdropIndex = i; + } + } + return topBackdropIndex; + } + + $rootScope.$watch(backdropIndex, function(newBackdropIndex){ + if (backdropScope) { + backdropScope.index = newBackdropIndex; + } + }); + + function removeModalWindow(modalInstance) { + + var body = $document.find('body').eq(0); + var modalWindow = openedWindows.get(modalInstance).value; + + //clean up the stack + openedWindows.remove(modalInstance); + + //remove window DOM element + removeAfterAnimate(modalWindow.modalDomEl, modalWindow.modalScope, 300, function() { + modalWindow.modalScope.$destroy(); + body.toggleClass(OPENED_MODAL_CLASS, openedWindows.length() > 0); + checkRemoveBackdrop(); + }); + } + + 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; + } + } + + function removeAfterAnimate(domEl, scope, emulateTime, done) { + // Closing animation + scope.animate = false; + + var transitionEndEventName = $transition.transitionEndEventName; + if (transitionEndEventName) { + // transition out + var timeout = $timeout(afterAnimating, emulateTime); + + domEl.bind(transitionEndEventName, function () { + $timeout.cancel(timeout); + afterAnimating(); + scope.$apply(); + }); + } else { + // Ensure this call is async + $timeout(afterAnimating); + } + + function afterAnimating() { + if (afterAnimating.done) { + return; + } + afterAnimating.done = true; + + domEl.remove(); + if (done) { + done(); + } + } + } + + $document.bind('keydown', function (evt) { + var modal; + + if (evt.which === 27) { + modal = openedWindows.top(); + if (modal && modal.value.keyboard) { + evt.preventDefault(); + $rootScope.$apply(function () { + $modalStack.dismiss(modal.key, 'escape key press'); + }); + } + } + }); + + $modalStack.open = function (modalInstance, modal) { + + openedWindows.add(modalInstance, { + deferred: modal.deferred, + modalScope: modal.scope, + backdrop: modal.backdrop, + keyboard: modal.keyboard + }); + + var body = $document.find('body').eq(0), + currBackdropIndex = backdropIndex(); + + if (currBackdropIndex >= 0 && !backdropDomEl) { + backdropScope = $rootScope.$new(true); + backdropScope.index = currBackdropIndex; + var angularBackgroundDomEl = angular.element('
      '); + angularBackgroundDomEl.attr('backdrop-class', modal.backdropClass); + backdropDomEl = $compile(angularBackgroundDomEl)(backdropScope); + body.append(backdropDomEl); + } + + var angularDomEl = angular.element('
      '); + angularDomEl.attr({ + 'template-url': modal.windowTemplateUrl, + 'window-class': modal.windowClass, + 'size': modal.size, + 'index': openedWindows.length() - 1, + '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); + if (modalWindow) { + modalWindow.value.deferred.resolve(result); + removeModalWindow(modalInstance); + } + }; + + $modalStack.dismiss = function (modalInstance, reason) { + var modalWindow = openedWindows.get(modalInstance); + if (modalWindow) { + modalWindow.value.deferred.reject(reason); + removeModalWindow(modalInstance); + } + }; + + $modalStack.dismissAll = function (reason) { + var topModal = this.getTop(); + while (topModal) { + this.dismiss(topModal.key, reason); + topModal = this.getTop(); + } + }; + + $modalStack.getTop = function () { + return openedWindows.top(); + }; + + return $modalStack; + }]) + + .provider('$modal', function () { + + var $modalProvider = { + options: { + backdrop: true, //can be also false or 'static' + keyboard: true + }, + $get: ['$injector', '$rootScope', '$q', '$http', '$templateCache', '$controller', '$modalStack', + function ($injector, $rootScope, $q, $http, $templateCache, $controller, $modalStack) { + + var $modal = {}; + + function getTemplatePromise(options) { + return options.template ? $q.when(options.template) : + $http.get(angular.isFunction(options.templateUrl) ? (options.templateUrl)() : options.templateUrl, + {cache: $templateCache}).then(function (result) { + return result.data; + }); + } + + function getResolvePromises(resolves) { + var promisesArr = []; + angular.forEach(resolves, function (value) { + if (angular.isFunction(value) || angular.isArray(value)) { + promisesArr.push($q.when($injector.invoke(value))); + } + }); + return promisesArr; + } + + $modal.open = function (modalOptions) { + + var modalResultDeferred = $q.defer(); + var modalOpenedDeferred = $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, + close: function (result) { + $modalStack.close(modalInstance, result); + }, + dismiss: function (reason) { + $modalStack.dismiss(modalInstance, reason); + } + }; + + //merge and clean up options + modalOptions = angular.extend({}, $modalProvider.options, modalOptions); + modalOptions.resolve = modalOptions.resolve || {}; + + //verify options + if (!modalOptions.template && !modalOptions.templateUrl) { + throw new Error('One of template or templateUrl options is required.'); + } + + var templateAndResolvePromise = + $q.all([getTemplatePromise(modalOptions)].concat(getResolvePromises(modalOptions.resolve))); + + + templateAndResolvePromise.then(function resolveSuccess(tplAndVars) { + + var modalScope = (modalOptions.scope || $rootScope).$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++]; + }); + + ctrlInstance = $controller(modalOptions.controller, ctrlLocals); + if (modalOptions.controllerAs) { + modalScope[modalOptions.controllerAs] = ctrlInstance; + } + } + + $modalStack.open(modalInstance, { + scope: modalScope, + deferred: modalResultDeferred, + content: tplAndVars[0], + backdrop: modalOptions.backdrop, + keyboard: modalOptions.keyboard, + backdropClass: modalOptions.backdropClass, + windowClass: modalOptions.windowClass, + windowTemplateUrl: modalOptions.windowTemplateUrl, + size: modalOptions.size + }); + + }, function resolveError(reason) { + modalResultDeferred.reject(reason); + }); + + templateAndResolvePromise.then(function () { + modalOpenedDeferred.resolve(true); + }, function () { + modalOpenedDeferred.reject(false); + }); + + 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) { + 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]; + + if (!ngModelCtrl) { + return; // do nothing if no ng-model + } + + // 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 + }; + } + + 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, number, number === currentPage); + pages.push(page); + } + + // Add links to move between page sets + if ( isMaxSized && ! rotate ) { + if ( startPage > 1 ) { + var previousPageSet = makePage(startPage - 1, '...', false); + pages.unshift(previousPageSet); + } + + 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); + } + }; + } + }; +}]) + +.constant('pagerConfig', { + itemsPerPage: 10, + previousText: '« Previous', + nextText: 'Next »', + align: true +}) + +.directive('pager', ['pagerConfig', function(pagerConfig) { + return { + restrict: 'EA', + scope: { + totalItems: '=', + previousText: '@', + nextText: '@' + }, + 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]; + + if (!ngModelCtrl) { + return; // do nothing if no ng-model + } + + scope.align = angular.isDefined(attrs.align) ? scope.$parent.$eval(attrs.align) : pagerConfig.align; + paginationCtrl.init(ngModelCtrl, pagerConfig); + } + }; +}]); + +/** + * The following features are still outstanding: animation as a + * 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' ] ) + +/** + * The $tooltip service creates tooltip- and popover-like directives as well as + * houses global options for them. + */ +.provider( '$tooltip', function () { + // The default options tooltip and popover. + var defaultOptions = { + placement: 'top', + animation: true, + popupDelay: 0 + }; + + // Default hide triggers for each show trigger + var triggerMap = { + 'mouseenter': 'mouseleave', + 'click': 'click', + 'focus': 'blur' + }; + + // The options specified to the provider globally. + var globalOptions = {}; + + /** + * `options({})` allows global configuration of all tooltips in the + * application. + * + * var app = angular.module( 'App', ['ui.bootstrap.tooltip'], function( $tooltipProvider ) { + * // place tooltips left instead of top by default + * $tooltipProvider.options( { placement: 'left' } ); + * }); + */ + 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' ); + */ + this.setTriggers = function setTriggers ( triggers ) { + angular.extend( triggerMap, triggers ); + }; + + /** + * This is a helper function for translating camel-case to snake-case. + */ + function snake_case(name){ + var regexp = /[A-Z]/g; + var separator = '-'; + return name.replace(regexp, function(letter, pos) { + return (pos ? separator : '') + letter.toLowerCase(); + }); + } + + /** + * 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 ); + + /** + * Returns an object of show and hide triggers. + * + * If a trigger is supplied, + * it is used to show the tooltip; otherwise, it will use the `trigger` + * option passed to the `$tooltipProvider.options` method; else it will + * default to the trigger supplied to this directive factory. + * + * The hide trigger is based on the show trigger. If the `trigger` option + * was passed to the `$tooltipProvider.options` method, it will use the + * mapped trigger from `triggerMap` or the passed trigger if the map is + * 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; + return { + show: show, + hide: hide + }; + } + + var directiveName = snake_case( type ); + + var startSym = $interpolate.startSymbol(); + var endSym = $interpolate.endSymbol(); + var template = + '
      '+ + '
      '; + + return { + restrict: 'EA', + scope: true, + compile: function (tElem, tAttrs) { + var tooltipLinker = $compile( template ); + + return function link ( scope, element, attrs ) { + var tooltip; + 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 positionTooltip = function () { + + var ttPosition = $position.positionElements(element, tooltip, scope.tt_placement, appendToBody); + ttPosition.top += 'px'; + ttPosition.left += 'px'; + + // Now set the calculated positioning. + tooltip.css( ttPosition ); + }; + + // By default, the tooltip is not open. + // TODO add ability to start tooltip opened + scope.tt_isOpen = false; + + function toggleTooltipBind () { + if ( ! scope.tt_isOpen ) { + showTooltipBind(); + } else { + hideTooltipBind(); + } + } + + // Show the tooltip with delay if specified, otherwise show it immediately + function showTooltipBind() { + if(hasEnableExp && !scope.$eval(attrs[prefix+'Enable'])) { + return; + } + if ( scope.tt_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();}); + } + } else { + show()(); + } + } + + function hideTooltipBind () { + scope.$apply(function () { + 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; + } + + // Don't show empty tooltips. + if ( ! scope.tt_content ) { + return angular.noop; + } + + createTooltip(); + + // Set the initial positioning. + tooltip.css({ top: 0, left: 0, display: 'block' }); + + // 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 ); + } + + 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; + } + + // Hide the tooltip popup element. + function hide() { + // 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); + } + } else { + removeTooltip(); + } + } + + function createTooltip() { + // There can only be one tooltip element per directive shown at once. + if (tooltip) { + removeTooltip(); + } + tooltip = tooltipLinker(scope, function () {}); + + // Get contents rendered into the tooltip + scope.$digest(); + } + + function removeTooltip() { + transitionTimeout = null; + if (tooltip) { + tooltip.remove(); + tooltip = null; + } + } + + /** + * Observe the relevant attributes. + */ + attrs.$observe( type, function ( val ) { + scope.tt_content = val; + + if (!val && scope.tt_isOpen ) { + hide(); + } + }); + + attrs.$observe( prefix+'Title', function ( val ) { + scope.tt_title = val; + }); + + attrs.$observe( prefix+'Placement', function ( val ) { + scope.tt_placement = angular.isDefined( val ) ? val : options.placement; + }); + + attrs.$observe( prefix+'PopupDelay', function ( val ) { + var delay = parseInt( val, 10 ); + scope.tt_popupDelay = ! isNaN(delay) ? delay : options.popupDelay; + }); + + var unregisterTriggers = function () { + element.unbind(triggers.show, showTooltipBind); + element.unbind(triggers.hide, hideTooltipBind); + }; + + attrs.$observe( prefix+'Trigger', function ( val ) { + unregisterTriggers(); + + triggers = getTriggers( val ); + + if ( triggers.show === triggers.hide ) { + element.bind( triggers.show, toggleTooltipBind ); + } else { + element.bind( triggers.show, showTooltipBind ); + element.bind( triggers.hide, hideTooltipBind ); + } + }); + + var animation = scope.$eval(attrs[prefix + 'Animation']); + scope.tt_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(); + } + }); + } + + // Make sure tooltip is destroyed and removed. + scope.$on('$destroy', function onDestroyTooltip() { + $timeout.cancel( transitionTimeout ); + $timeout.cancel( popupTimeout ); + unregisterTriggers(); + removeTooltip(); + }); + }; + } + }; + }; + }]; +}) + +.directive( 'tooltipPopup', function () { + return { + restrict: 'EA', + replace: true, + scope: { content: '@', placement: '@', animation: '&', isOpen: '&' }, + templateUrl: 'template/tooltip/tooltip-popup.html' + }; +}) + +.directive( 'tooltip', [ '$tooltip', function ( $tooltip ) { + return $tooltip( 'tooltip', 'tooltip', 'mouseenter' ); +}]) + +.directive( 'tooltipHtmlUnsafePopup', function () { + return { + restrict: 'EA', + replace: true, + scope: { content: '@', placement: '@', animation: '&', isOpen: '&' }, + templateUrl: 'template/tooltip/tooltip-html-unsafe-popup.html' + }; +}) + +.directive( 'tooltipHtmlUnsafe', [ '$tooltip', function ( $tooltip ) { + return $tooltip( 'tooltipHtmlUnsafe', 'tooltip', 'mouseenter' ); +}]); + +/** + * 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. + */ +angular.module( 'ui.bootstrap.popover', [ 'ui.bootstrap.tooltip' ] ) + +.directive( 'popoverPopup', function () { + return { + restrict: 'EA', + replace: true, + scope: { title: '@', content: '@', placement: '@', animation: '&', isOpen: '&' }, + templateUrl: 'template/popover/popover.html' + }; +}) + +.directive( 'popover', [ '$tooltip', function ( $tooltip ) { + return $tooltip( 'popover', 'popover', 'click' ); +}]); + +angular.module('ui.bootstrap.progressbar', []) + +.constant('progressConfig', { + 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; + + this.bars = []; + $scope.max = angular.isDefined($attrs.max) ? $scope.$parent.$eval($attrs.max) : progressConfig.max; + + this.addBar = function(bar, element) { + if ( !animate ) { + element.css({'transition': 'none'}); + } + + this.bars.push(bar); + + bar.$watch('value', function( value ) { + bar.percent = +(100 * value / $scope.max).toFixed(2); + }); + + bar.$on('$destroy', function() { + element = null; + self.removeBar(bar); + }); + }; + + this.removeBar = function(bar) { + this.bars.splice(this.bars.indexOf(bar), 1); + }; +}]) + +.directive('progress', function() { + return { + restrict: 'EA', + replace: true, + transclude: true, + controller: 'ProgressController', + require: 'progress', + scope: {}, + templateUrl: '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('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])); + } + }; +}); +angular.module('ui.bootstrap.rating', []) + +.constant('ratingConfig', { + max: 5, + stateOn: null, + stateOff: null +}) + +.controller('RatingController', ['$scope', '$attrs', 'ratingConfig', function($scope, $attrs, ratingConfig) { + var ngModelCtrl = { $setViewValue: angular.noop }; + + this.init = function(ngModelCtrl_) { + ngModelCtrl = ngModelCtrl_; + ngModelCtrl.$render = this.render; + + 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; + + 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]); + } + return states; + }; + + $scope.rate = function(value) { + if ( !$scope.readonly && value >= 0 && value <= $scope.range.length ) { + ngModelCtrl.$setViewValue(value); + ngModelCtrl.$render(); + } + }; + + $scope.enter = function(value) { + if ( !$scope.readonly ) { + $scope.value = value; + } + $scope.onHover({value: value}); + }; + + $scope.reset = function() { + $scope.value = ngModelCtrl.$viewValue; + $scope.onLeave(); + }; + + $scope.onKeydown = function(evt) { + if (/(37|38|39|40)/.test(evt.which)) { + evt.preventDefault(); + evt.stopPropagation(); + $scope.rate( $scope.value + (evt.which === 38 || evt.which === 39 ? 1 : -1) ); + } + }; + + this.render = function() { + $scope.value = ngModelCtrl.$viewValue; + }; +}]) + +.directive('rating', function() { + return { + restrict: 'EA', + require: ['rating', 'ngModel'], + scope: { + readonly: '=?', + onHover: '&', + onLeave: '&' + }, + controller: 'RatingController', + templateUrl: 'template/rating/rating.html', + replace: true, + link: function(scope, element, attrs, ctrls) { + var ratingCtrl = ctrls[0], ngModelCtrl = ctrls[1]; + + if ( 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) { + var ctrl = this, + tabs = ctrl.tabs = $scope.tabs = []; + + ctrl.select = function(selectedTab) { + angular.forEach(tabs, function(tab) { + if (tab.active && tab !== selectedTab) { + tab.active = false; + tab.onDeselect(); + } + }); + selectedTab.active = true; + selectedTab.onSelect(); + }; + + 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.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]); + } + tabs.splice(index, 1); + }; +}]) + +/** + * @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() { + return { + restrict: 'EA', + transclude: true, + replace: true, + scope: { + type: '@' + }, + controller: 'TabsetController', + templateUrl: '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; + } + }; +}) + +/** + * @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) { + return { + require: '^tabset', + restrict: 'EA', + replace: true, + templateUrl: 'template/tabs/tab.html', + transclude: true, + scope: { + active: '=?', + heading: '@', + onSelect: '&select', //This callback is called in contentHeadingTransclude + //once it inserts the tab's content into the dom + onDeselect: '&deselect' + }, + 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); + } + }); + + scope.disabled = false; + if ( attrs.disabled ) { + scope.$parent.$watch($parse(attrs.disabled), function(value) { + scope.disabled = !! value; + }); + } + + scope.select = function() { + if ( !scope.disabled ) { + scope.active = true; + } + }; + + 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() { + return { + restrict: 'A', + require: '^tab', + link: function(scope, elm, attrs, tabCtrl) { + scope.$watch('headingElement', function updateHeadingElement(heading) { + if (heading) { + elm.html(''); + elm.append(heading); + } + }); + } + }; +}]) + +.directive('tabContentTransclude', function() { + return { + restrict: 'A', + require: '^tabset', + link: function(scope, elm, attrs) { + var tab = scope.$eval(attrs.tabContentTransclude); + + //Now our tab is ready to be transcluded: both the tab heading area + //and the tab content area are loaded. Transclude 'em both. + tab.$transcludeFn(tab.$parent, function(contents) { + angular.forEach(contents, function(node) { + if (isTabHeading(node)) { + //Let tabHeadingTransclude know. + tab.headingElement = node; + } else { + elm.append(node); + } + }); + }); + } + }; + 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' + ); + } +}) + +; + +angular.module('ui.bootstrap.timepicker', []) + +.constant('timepickerConfig', { + hourStep: 1, + minuteStep: 1, + showMeridian: true, + meridians: null, + readonlyInput: false, + mousewheel: true +}) + +.controller('TimepickerController', ['$scope', '$attrs', '$parse', '$log', '$locale', 'timepickerConfig', function($scope, $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; + + this.init = function( ngModelCtrl_, inputs ) { + ngModelCtrl = ngModelCtrl_; + ngModelCtrl.$render = this.render; + + var hoursInputEl = inputs.eq(0), + minutesInputEl = inputs.eq(1); + + var mousewheel = angular.isDefined($attrs.mousewheel) ? $scope.$parent.$eval($attrs.mousewheel) : timepickerConfig.mousewheel; + if ( mousewheel ) { + this.setupMousewheelEvents( hoursInputEl, minutesInputEl ); + } + + $scope.readonlyInput = angular.isDefined($attrs.readonlyInput) ? $scope.$parent.$eval($attrs.readonlyInput) : timepickerConfig.readonlyInput; + this.setupInputEvents( hoursInputEl, minutesInputEl ); + }; + + var hourStep = timepickerConfig.hourStep; + if ($attrs.hourStep) { + $scope.$parent.$watch($parse($attrs.hourStep), function(value) { + hourStep = parseInt(value, 10); + }); + } + + var minuteStep = timepickerConfig.minuteStep; + if ($attrs.minuteStep) { + $scope.$parent.$watch($parse($attrs.minuteStep), function(value) { + minuteStep = parseInt(value, 10); + }); + } + + // 12H / 24H mode + $scope.showMeridian = timepickerConfig.showMeridian; + if ($attrs.showMeridian) { + $scope.$parent.$watch($parse($attrs.showMeridian), function(value) { + $scope.showMeridian = !!value; + + if ( ngModelCtrl.$error.time ) { + // Evaluate from template + var hours = getHoursFromTemplate(), minutes = getMinutesFromTemplate(); + 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 ) { + return undefined; + } + + if ( $scope.showMeridian ) { + if ( hours === 12 ) { + hours = 0; + } + if ( $scope.meridian === meridians[1] ) { + hours = hours + 12; + } + } + return hours; + } + + function getMinutesFromTemplate() { + var minutes = parseInt($scope.minutes, 10); + return ( minutes >= 0 && minutes < 60 ) ? minutes : undefined; + } + + function pad( value ) { + return ( angular.isDefined(value) && value.toString().length < 2 ) ? '0' + value : value; + } + + // Respond on mousewheel spin + this.setupMousewheelEvents = function( hoursInputEl, minutesInputEl ) { + 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); + }; + + hoursInputEl.bind('mousewheel wheel', function(e) { + $scope.$apply( (isScrollingUp(e)) ? $scope.incrementHours() : $scope.decrementHours() ); + e.preventDefault(); + }); + + minutesInputEl.bind('mousewheel wheel', function(e) { + $scope.$apply( (isScrollingUp(e)) ? $scope.incrementMinutes() : $scope.decrementMinutes() ); + e.preventDefault(); + }); + + }; + + this.setupInputEvents = function( hoursInputEl, minutesInputEl ) { + if ( $scope.readonlyInput ) { + $scope.updateHours = angular.noop; + $scope.updateMinutes = angular.noop; + return; + } + + var invalidate = function(invalidHours, invalidMinutes) { + ngModelCtrl.$setViewValue( null ); + ngModelCtrl.$setValidity('time', false); + if (angular.isDefined(invalidHours)) { + $scope.invalidHours = invalidHours; + } + if (angular.isDefined(invalidMinutes)) { + $scope.invalidMinutes = invalidMinutes; + } + }; + + $scope.updateHours = function() { + var hours = getHoursFromTemplate(); + + if ( angular.isDefined(hours) ) { + selected.setHours( hours ); + refresh( 'h' ); + } else { + invalidate(true); + } + }; + + hoursInputEl.bind('blur', function(e) { + if ( !$scope.invalidHours && $scope.hours < 10) { + $scope.$apply( function() { + $scope.hours = pad( $scope.hours ); + }); + } + }); + + $scope.updateMinutes = function() { + var minutes = getMinutesFromTemplate(); + + if ( angular.isDefined(minutes) ) { + selected.setMinutes( minutes ); + refresh( 'm' ); + } else { + invalidate(undefined, true); + } + }; + + minutesInputEl.bind('blur', function(e) { + if ( !$scope.invalidMinutes && $scope.minutes < 10 ) { + $scope.$apply( function() { + $scope.minutes = pad( $scope.minutes ); + }); + } + }); + + }; + + this.render = function() { + var date = ngModelCtrl.$modelValue ? new Date( ngModelCtrl.$modelValue ) : null; + + 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 ) { + selected = date; + } + makeValid(); + updateTemplate(); + } + }; + + // Call internally when we know that model is valid. + function refresh( keyboardChange ) { + makeValid(); + ngModelCtrl.$setViewValue( new Date(selected) ); + updateTemplate( keyboardChange ); + } + + function makeValid() { + ngModelCtrl.$setValidity('time', true); + $scope.invalidHours = false; + $scope.invalidMinutes = false; + } + + function updateTemplate( keyboardChange ) { + var hours = selected.getHours(), minutes = selected.getMinutes(); + + if ( $scope.showMeridian ) { + hours = ( hours === 0 || hours === 12 ) ? 12 : hours % 12; // Convert 24 to 12 hour system + } + + $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() ); + refresh(); + } + + $scope.incrementHours = function() { + addMinutes( hourStep * 60 ); + }; + $scope.decrementHours = function() { + addMinutes( - hourStep * 60 ); + }; + $scope.incrementMinutes = function() { + addMinutes( minuteStep ); + }; + $scope.decrementMinutes = function() { + addMinutes( - minuteStep ); + }; + $scope.toggleMeridian = function() { + addMinutes( 12 * 60 * (( selected.getHours() < 12 ) ? 1 : -1) ); + }; +}]) + +.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') ); + } + } + }; +}); + +angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap.bindHtml']) + +/** + * 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\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]) + }; + } + }; +}]) + + .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)); + } + }); + + var getMatchesAsync = function(inputValue) { + + var locals = {$viewValue: inputValue}; + isLoadingSetter(originalScope, true); + $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.length > 0) { + + scope.activeIdx = 0; + scope.matches.length = 0; + + //transform labels + for(var i=0; i= minSearch) { + if (waitTime > 0) { + 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; + } + } + }); + + modelCtrl.$formatters.push(function (modelValue) { + + var candidateViewValue, emptyViewValue; + var locals = {}; + + if (inputFormatter) { + + 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(); + + 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(); + } + }); + + 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 () { + 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}); + }; + } + }; + }) + + .directive('typeaheadMatch', ['$http', '$templateCache', '$compile', '$parse', function ($http, $templateCache, $compile, $parse) { + return { + restrict:'EA', + scope:{ + index:'=', + match:'=', + query:'=' + }, + 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)); + }); + } + }; + }]) + + .filter('typeaheadHighlight', function() { + + function escapeRegexp(queryToEscape) { + return queryToEscape.replace(/([.?*+^$[\]\\(){}|-])/g, '\\$1'); + } + + return function(matchItem, query) { + return query ? ('' + matchItem).replace(new RegExp(escapeRegexp(query), 'gi'), '$&') : matchItem; + }; + }); diff --git a/src/components/angular-bootstrap/ui-bootstrap.min.js b/src/components/angular-bootstrap/ui-bootstrap.min.js new file mode 100644 index 00000000..ffd1a9e0 --- /dev/null +++ b/src/components/angular-bootstrap/ui-bootstrap.min.js @@ -0,0 +1,9 @@ +/* + * angular-ui-bootstrap + * http://angular-ui.github.io/bootstrap/ + + * Version: 0.11.2 - 2014-09-26 + * 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){var c=[],d=a.split("");return angular.forEach(e,function(b,e){var f=a.indexOf(e);if(f>-1){a=a.split(""),d[f]="("+b.regex+")",a[f]="$";for(var g=f+1,h=f+e.length;h>g;g++)d[g]="",a[g]="$";a=a.join(""),c.push({index:f,apply:b.apply})}}),{regex:new RegExp("^"+d.join("")+"$"),map:b(c,"index")}}function d(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 e={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.parse=function(b,e){if(!angular.isString(b)||!e)return b;e=a.DATETIME_FORMATS[e]||e,this.parsers[e]||(this.parsers[e]=c(e));var f=this.parsers[e],g=f.regex,h=f.map,i=b.match(g);if(i&&i.length){for(var j,k={year:1900,month:0,date:1,hours:0},l=1,m=i.length;m>l;l++){var n=h[l-1];n.apply&&n.apply.call(k,i[l])}return d(k.year,k.month,k.date)&&(j=new Date(k.year,k.month,k.date,k.hours)),j}}}]),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)}),h.watchData={},angular.forEach(["minDate","maxDate","datepickerMode"],function(a){if(j[a]){var c=b(j[a]);if(h.$parent.$watch(c,function(b){h.watchData[a]=b}),r.attr(l(a),"watchData."+a),"datepickerMode"===a){var d=c.assign;h.$watch("watchData."+a,function(a,b){a!==b&&d(h.$parent,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);q.remove(),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){var c=b.getToggleElement();a&&c&&c[0].contains(a.target)||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.getToggleElement=function(){return h.toggleElement},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)}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();if(h>=0&&!k){l=e.$new(!0),l.index=h;var i=angular.element("
      ");i.attr("backdrop-class",b.backdropClass),k=d(i)(l),f.append(k)}var j=angular.element("
      ");j.attr({"template-url":b.windowTemplateUrl,"window-class":b.windowClass,size:b.size,index:n.length()-1,animate:"animate"}).html(b.content);var o=d(j)(b.scope);n.top().value.modalDomEl=o,f.append(o),f.addClass(m)},o.close=function(a,b){var c=n.get(a);c&&(c.value.deferred.resolve(b),h(a))},o.dismiss=function(a,b){var c=n.get(a);c&&(c.value.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(angular.isFunction(a.templateUrl)?a.templateUrl():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),b.controllerAs&&(d[b.controllerAs]=f)),h.open(k,{scope:d,deferred:e,content:a[0],backdrop:b.backdrop,keyboard:b.keyboard,backdropClass:b.backdropClass,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\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])}}}}]).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?(E(),D(a)):B(a):(q(i,!1),E(),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 F=function(a){j[0]!==a.target&&(z(),w.$digest())};e.bind("click",F),i.$on("$destroy",function(){e.unbind("click",F)});var G=a(y)(w);t?e.find("body").append(G):j.after(G)}}}]).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 From d88afe9c37852ce41db248a0859cd6bcb51ea05e Mon Sep 17 00:00:00 2001 From: rikkertkoppes Date: Mon, 24 Nov 2014 12:26:34 +0100 Subject: [PATCH 10/29] no spaces in filenames --- challenges/xml/{2014_nl_NL no-enum.xml => 2014_nl_NL-no-enum.xml} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename challenges/xml/{2014_nl_NL no-enum.xml => 2014_nl_NL-no-enum.xml} (100%) diff --git a/challenges/xml/2014_nl_NL no-enum.xml b/challenges/xml/2014_nl_NL-no-enum.xml similarity index 100% rename from challenges/xml/2014_nl_NL no-enum.xml rename to challenges/xml/2014_nl_NL-no-enum.xml From 2786d4bdf70bfe714328a5d2fa12918f40d1827c Mon Sep 17 00:00:00 2001 From: rikkertkoppes Date: Mon, 24 Nov 2014 12:27:00 +0100 Subject: [PATCH 11/29] updated readme to clarify things and have a more natural order --- readme.md | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/readme.md b/readme.md index f1e4273e..c82c4137 100644 --- a/readme.md +++ b/readme.md @@ -20,7 +20,21 @@ Steps - Clone the repository - `npm install` -- `bower install` (this should be done automatically by npm install) + +Run local +-------- + +This is mainly used for development. + +- `node localserver.js` then open [localhost:1390](http://localhost:1390) + - to specify another port, use `node localserver.js -p 8000` + - to add basic authentication, use `node localserver.js -u username:password` + +Testing +------- + +- run `karma start` +- or run `grunt karma` Building -------- @@ -37,19 +51,6 @@ To build js challenge files from the xml description files, use node tools\buildchallenge.js challenges\xml\2014.xml > challenges\js\2014.js -Run local --------- - -- `node localserver.js` then open [localhost:1390](http://localhost:1390) - - to specify another port, use `node localserver.js -p 8000` - - to add basic authentication, use `node localserver.js -u username:password` - -Testing -------- - -- run `karma start` -- or run `grunt karma` - Documentation ------------- From 666af31db342273452f5e6026b974bd75bf4b8a9 Mon Sep 17 00:00:00 2001 From: rikkertkoppes Date: Mon, 24 Nov 2014 12:30:41 +0100 Subject: [PATCH 12/29] updated 2014 js, html and pdf --- challenges/html/2014_nl_NL-no-enum.html | 524 ++++++++++++++ challenges/html/2014_nl_NL.html | 2 +- challenges/js/2014_nl_NL-no-enum.js | 884 ++++++++++++++++++++++++ challenges/pdf/2012.pdf | Bin 34039 -> 34039 bytes challenges/pdf/2014.pdf | Bin 37812 -> 37812 bytes challenges/pdf/2014_nl_NL-no-enum.pdf | Bin 0 -> 38085 bytes challenges/pdf/2014_nl_NL.pdf | Bin 37962 -> 37942 bytes 7 files changed, 1409 insertions(+), 1 deletion(-) create mode 100644 challenges/html/2014_nl_NL-no-enum.html create mode 100644 challenges/js/2014_nl_NL-no-enum.js create mode 100644 challenges/pdf/2014_nl_NL-no-enum.pdf diff --git a/challenges/html/2014_nl_NL-no-enum.html b/challenges/html/2014_nl_NL-no-enum.html new file mode 100644 index 00000000..95cca782 --- /dev/null +++ b/challenges/html/2014_nl_NL-no-enum.html @@ -0,0 +1,524 @@ + + + + +

      World Class

      + + + + + + +
      +

      Reverse Engineering

      + +
      Mand in de basisYesNo
      + +
      Jullie model ligt in de basis en is ‘identiek’YesNo
      + + + + + + + + + + + + + +
      + + +
      +

      Deuren openen

      + +
      Deur geopend door de hendel omlaag te drukken YesNo
      + + + + + + + + + + +
      + + +
      +

      Projectonderwijs

      + +
      Lussen aan de weegschaal 0 1 2 3 4 5 6 7 8
      + + + + + + + + + + + + + + + + + +
      + + +
      +

      Stagelopen

      + +
      Model getoond aan de scheidsrechterYesNo
      + +
      Raakt de cirkel, niet in de basis en poppetjes verbondenYesNo
      + + + + + + + + + + + + + +
      + + +
      +

      Zoekmachine

      + +
      Alleen de schuif heeft het wiel 1+ keer rondgedraaidYesNo
      + +
      Alleen de juiste lus is verwijderdYesNo
      + + + + + + + + + + + + + +
      + + +
      +

      Sport

      + +
      Schot genomen vanuit positie Noord-Oost van de lijn YesNo
      + +
      Bal raakt de mat in het doel aan het eind van de wedstrijdYesNo
      + + + + + + + + + + + + + +
      + + +
      +

      Robotwedstrijden

      + +
      Alleen het robotelement is geïnstalleerd in de robotarmYesNo
      + +
      Lus raakt de robotarm niet aanYesNo
      + + + + + + + + + + + + + +
      + + +
      +

      Gebruik de juiste zintuigen en leerstijlen

      + +
      Lus raakt het model niet aanYesNo
      + + + + + + + + + + +
      + + +
      +

      Leren/communiceren op afstand

      + +
      Scheids zag de robot de schuif verplaatsenYesNo
      + + + + + + + + + + +
      + + +
      +

      Outside the Box denken

      + +
      Idee-model raakt de doos niet, Doos niet in basis geweest, Lamp is + naar boven gerichtYesNo
      + +
      Idee-model raakt de doos niet, Doos niet in basis geweest, Lamp is + naar beneden gerichtYesNo
      + + + + + + + + + + + + + +
      + + +
      +

      Gemeenschappelijk leren

      + +
      Lus raakt het model niet aanYesNo
      + + + + + + + + + + +
      + + +
      +

      Cloud toegang

      + +
      SD card staat omhoog omdat de juiste "key" is ingebrachtYesNo
      + + + + + + + + + + +
      + + +
      +

      Betrokkenheid

      + +
      Gele gedeelte is naar het zuiden verplaatstYesNo
      + +
      Aangewezen kleur NVT Rood 10% Oranje 16% Groen 22% Blauw 28% Rood 34% Blauw 40% Groen 46% Oranje 52% Rood 58%
      + +
      Klikken voorbij de kleur NVT 0 1 2 3 4 5
      + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      + + +
      +

      Flexibiliteit

      + +
      Model is 90 graden tegen de klok in gedraaid YesNo
      + + + + + + + + + + +
      + + +
      +

      Strafpunten

      + +
      Robot, rommel of uitvouwstrafpunten 0 1 2 3 4 5 6 7 8
      + + + + + + + + + + + + + + + + + + +
      + + + + \ No newline at end of file diff --git a/challenges/html/2014_nl_NL.html b/challenges/html/2014_nl_NL.html index 317be0fb..fffdea2f 100644 --- a/challenges/html/2014_nl_NL.html +++ b/challenges/html/2014_nl_NL.html @@ -246,7 +246,7 @@

      Outside the Box denken

      naar boven gerichtYesNo
      Idee-model raakt de doos niet, Doos niet in basis geweest, Lamp is - naar boven gerichtYesNo
      + naar beneden gerichtYesNo diff --git a/challenges/js/2014_nl_NL-no-enum.js b/challenges/js/2014_nl_NL-no-enum.js new file mode 100644 index 00000000..d7554c66 --- /dev/null +++ b/challenges/js/2014_nl_NL-no-enum.js @@ -0,0 +1,884 @@ +{ + "title": "World Class", + "missions": [{ + "title": "Reverse Engineering", + "description": "De zichtbare situatie aan het einde van de wedstrijd:
      • Jullie mand is in de basis.
      • Het model raakt de witte cirkel rond het projectonderwijs-model aan.
      • Jullie hebben een model gemaakt ‘identiek’ aan het model dat het andere team in jullie mand heeft gedaan. De verbindingen tussen de elementen moeten hetzelfde zijn, maar elementen mogen wel ‘gedraaid’ zitten.
      • Het model is in de basis.
      Vereiste methode en beperkingen:
      • Geen.
      ", + "objectives": [{ + "id": "basket", + "title": "Mand in de basis", + "type": "yesno" + }, { + "id": "identical", + "title": "Jullie model ligt in de basis en is ‘identiek’", + "type": "yesno" + }], + "score": [function(basket, identical) { + if (basket === 'no' && identical === 'no') { + return 0 + } + if (basket === 'no' && identical === 'yes') { + return 0 + } + if (basket === 'yes' && identical === 'no') { + return 30 + } + if (basket === 'yes' && identical === 'yes') { + return 45 + } + }] + }, { + "title": "Deuren openen", + "description": "De zichtbare situatie aan het einde van de wedstrijd:
      • De deur moet ver genoeg geopend zijn zodat de scheidsrechter dit kan zien.
      Vereiste methode en beperkingen:
      • De hendel moet omlaag gedrukt zijn.
      ", + "objectives": [{ + "id": "dooropen", + "title": "Deur geopend door de hendel omlaag te drukken", + "type": "yesno" + }], + "score": [function(dooropen) { + if (dooropen === 'no') { + return 0 + } + if (dooropen === 'yes') { + return 15 + } + }] + }, { + "title": "Projectonderwijs", + "description": "De zichtbare situatie aan het einde van de wedstrijd:
      • De lussen (welke symbool staan voor kennis en vaardigheden) hangen aan de weegschaal zoals getoond.
      Vereiste methode en beperkingen:
      • Geen.
      ", + "objectives": [{ + "id": "loops", + "title": "Lussen aan de weegschaal", + "options": [{ + "value": "0", + "title": "0" + }, { + "value": "1", + "title": "1" + }, { + "value": "2", + "title": "2" + }, { + "value": "3", + "title": "3" + }, { + "value": "4", + "title": "4" + }, { + "value": "5", + "title": "5" + }, { + "value": "6", + "title": "6" + }, { + "value": "7", + "title": "7" + }, { + "value": "8", + "title": "8" + }], + "type": "enum" + }], + "score": [function(loops) { + if (loops === '0') { + return 0 + } + if (loops === '1') { + return 20 + } + if (loops === '2') { + return 30 + } + if (loops === '3') { + return 40 + } + if (loops === '4') { + return 50 + } + if (loops === '5') { + return 60 + } + if (loops === '6') { + return 70 + } + if (loops === '7') { + return 80 + } + if (loops === '8') { + return 90 + } + }] + }, { + "title": "Stagelopen", + "description": "De zichtbare situatie aan het einde van de wedstrijd:
      • De LEGO-poppetjes zijn beiden verbonden (op een manier naar keuze) aan een model dat jullie ontwerpen en meenemen. Dit model stelt een vaardigheid, prestatie, carrière of hobby voor dat een speciale betekenis voor jullie team heeft.
      • Het model raakt de witte cirkel rond het projectonderwijs-model aan.
      • Het model is niet in de basis.
      • Het vastmaken van missiemodellen is normaal niet toegestaan vanwege regel 39.4, deze missie is daar een uitzondering op.
      • Het eigen model mag simpel, of complex zijn, het mag primitief of realistisch zijn, de keuze is aan jullie. De keuze wat voor model jullie bouwen, heeft geen invloed op de score.
      Vereiste methode en beperkingen:
      • Geen.
      ", + "objectives": [{ + "id": "modelshown", + "title": "Model getoond aan de scheidsrechter", + "type": "yesno" + }, { + "id": "touchingcicrle", + "title": "Raakt de cirkel, niet in de basis en poppetjes verbonden", + "type": "yesno" + }], + "score": [function(modelshown, touchingcicrle) { + if (modelshown === 'no' && touchingcicrle === 'no') { + return 0 + } + if (modelshown === 'no' && touchingcicrle === 'yes') { + return new Error("Model moet getoond zijn voordat het de cirkel kan aanraken") + } + if (modelshown === 'yes' && touchingcicrle === 'no') { + return 20 + } + if (modelshown === 'yes' && touchingcicrle === 'yes') { + return 35 + } + }] + }, { + "title": "Zoekmachine", + "description": "De zichtbare situatie aan het einde van de wedstrijd:
      • Het kleurenwiel heeft minimaal een keer gedraaid.
      • Als één kleur verschijnt in het witte frame, dan raakt de lus van de zichtbare kleur het model niet meer aan.
      • Als twee kleuren verschijnen in het witte venster, dan is de lus van de kleur die niet zichtbaar is in het venster, de lus die het model niet meer raakt.
      • Beide lussen die niet verwijderd dienen te worden raken via ‘hun’ gaten het model aan.
      Vereiste methode en beperkingen:
      • Alleen de beweging van de schuif heeft het kleurenwiel in beweging gebracht.
      ", + "objectives": [{ + "id": "wheelspin", + "title": "Alleen de schuif heeft het wiel 1+ keer rondgedraaid", + "type": "yesno" + }, { + "id": "searchloop", + "title": "Alleen de juiste lus is verwijderd", + "type": "yesno" + }], + "score": [function(wheelspin, searchloop) { + if (wheelspin === 'no' && searchloop === 'no') { + return 0 + } + if (wheelspin === 'no' && searchloop === 'yes') { + return 0 + } + if (wheelspin === 'yes' && searchloop === 'no') { + return 15 + } + if (wheelspin === 'yes' && searchloop === 'yes') { + return 60 + } + }] + }, { + "title": "Sport", + "description": "De zichtbare situatie aan het einde van de wedstrijd:
      • De bal raakt de mat in het doel.
      Vereiste methode en beperkingen:
      • Alle onderdelen die met het schot te maken hebben waren volledig ten noordoosten van de ‘schietlijn’ op het moment dat de bal werd losgelaten richting het doel.
      ", + "objectives": [{ + "id": "ballshot", + "title": "Schot genomen vanuit positie Noord-Oost van de lijn", + "type": "yesno" + }, { + "id": "ballscored", + "title": "Bal raakt de mat in het doel aan het eind van de wedstrijd", + "type": "yesno" + }], + "score": [function(ballshot, ballscored) { + if (ballshot === 'no' && ballscored === 'no') { + return 0 + } + if (ballshot === 'no' && ballscored === 'yes') { + return 0 + } + if (ballshot === 'yes' && ballscored === 'no') { + return 30 + } + if (ballshot === 'yes' && ballscored === 'yes') { + return 60 + } + }] + }, { + "title": "Robotwedstrijden", + "description": "De zichtbare situatie aan het einde van de wedstrijd:
      • Het blauw-geel-rode robotelement (model) is geïnstalleerd in de robotarm zoals zichtbaar op de afbeelding.
      • De lus raakt niet langer de robotarm aan.
      Vereiste methode en beperkingen:
      • Geen strategisch object raakt de robotarm aan.
      • De lus werd alleen door het gebruik van de zwarte schuif losgemaakt.
      ", + "objectives": [{ + "id": "roboticsinsert", + "title": "Alleen het robotelement is geïnstalleerd in de robotarm", + "type": "yesno" + }, { + "id": "competitionloop", + "title": "Lus raakt de robotarm niet aan", + "type": "yesno" + }], + "score": [function(roboticsinsert, competitionloop) { + if (roboticsinsert === 'no' && competitionloop === 'no') { + return 0 + } + if (roboticsinsert === 'no' && competitionloop === 'yes') { + return 0 + } + if (roboticsinsert === 'yes' && competitionloop === 'no') { + return 25 + } + if (roboticsinsert === 'yes' && competitionloop === 'yes') { + return 55 + } + }] + }, { + "title": "Gebruik de juiste zintuigen en leerstijlen", + "description": "De zichtbare situatie aan het einde van de wedstrijd:
      • De lus raakt het zintuigen model niet meer aan.
      Vereiste methode en beperkingen:
      • De lus werd alleen door het gebruik van de schuif losgemaakt.
      ", + "objectives": [{ + "id": "sensesloop", + "title": "Lus raakt het model niet aan", + "type": "yesno" + }], + "score": [function(sensesloop) { + if (sensesloop === 'no') { + return 0 + } + if (sensesloop === 'yes') { + return 40 + } + }] + }, { + "title": "Leren/communiceren op afstand", + "description": "De zichtbare situatie aan het einde van de wedstrijd:
      • Geen.
      Vereiste methode en beperkingen:
      • De scheidsrechter heeft gezien dat de schuif door de robot westwaarts is verplaatst.
      ", + "objectives": [{ + "id": "pullslider", + "title": "Scheids zag de robot de schuif verplaatsen", + "type": "yesno" + }], + "score": [function(pullslider) { + if (pullslider === 'no') { + return 0 + } + if (pullslider === 'yes') { + return 40 + } + }] + }, { + "title": "Outside the Box denken", + "description": "De zichtbare situatie aan het einde van de wedstrijd:
      • Het ‘idee-model’ raakt niet langer het ‘doos-model’ aan.
      • Als het ‘idee-model’ het ‘doos-model’ niet meer aanraakt, is de afbeelding van de gloeilamp van bovenaf zichtbaar.
      Vereiste methode en beperkingen:
      • Het ‘doos-model’ is nooit in de basis geweest.
      ", + "objectives": [{ + "id": "bulbup", + "title": "Idee-model raakt de doos niet, Doos niet in basis geweest, Lamp is naar boven gericht", + "type": "yesno" + }, { + "id": "bulbdown", + "title": "Idee-model raakt de doos niet, Doos niet in basis geweest, Lamp is naar beneden gericht", + "type": "yesno" + }], + "score": [function(bulbup, bulbdown) { + if (bulbup === 'no' && bulbdown === 'no') { + return 0 + } + if (bulbup === 'no' && bulbdown === 'yes') { + return 25 + } + if (bulbup === 'yes' && bulbdown === 'no') { + return 40 + } + if (bulbup === 'yes' && bulbdown === 'yes') { + return new Error("De lamp kan niet tegelijk naar boven en beneden gericht zijn") + } + }] + }, { + "title": "Gemeenschappelijk leren", + "description": "De zichtbare situatie aan het einde van de wedstrijd:
      • De ‘kennis & vaardigheden lus’ raakt het gemeenschapsmodel niet meer aan.
      Vereiste methode en beperkingen:
      • Geen.
      ", + "objectives": [{ + "id": "communityloop", + "title": "Lus raakt het model niet aan", + "type": "yesno" + }], + "score": [function(communityloop) { + if (communityloop === 'no') { + return 0 + } + if (communityloop === 'yes') { + return 25 + } + }] + }, { + "title": "Cloud toegang", + "description": "De zichtbare situatie aan het einde van de wedstrijd:
      • De SD-kaart staat omhoog.
      Vereiste methode en beperkingen:
      • De juiste “key” is in de Cloud geplaatst.
      ", + "objectives": [{ + "id": "sdcardup", + "title": "SD card staat omhoog omdat de juiste \"key\" is ingebracht", + "type": "yesno" + }], + "score": [function(sdcardup) { + if (sdcardup === 'no') { + return 0 + } + if (sdcardup === 'yes') { + return 30 + } + }] + }, { + "title": "Betrokkenheid", + "description": "De zichtbare situatie aan het einde van de wedstrijd:
      • Het gele gedeelte is naar het zuiden verplaatst.
      • Het rad is duidelijk met de klok mee gedraaid ten opzichte van de start positie. Zie het overzicht voor de score.
      Vereiste methode en beperkingen:
      • De wijzer mag alleen verplaatst worden doordat de robot aan het rad te draait.
      • De robot mag het rad maar een keer 180⁰ draaien, per keer dat de basis wordt verlaten. De scheidsrechter zal extra draaiingen ongedaan maken
      ", + "objectives": [{ + "id": "yellow_moved", + "title": "Gele gedeelte is naar het zuiden verplaatst", + "type": "yesno" + }, { + "id": "dial_major_color", + "title": "Aangewezen kleur", + "options": [{ + "value": "na", + "title": "NVT" + }, { + "value": "red10", + "title": "Rood 10%" + }, { + "value": "orange16", + "title": "Oranje 16%" + }, { + "value": "green22", + "title": "Groen 22%" + }, { + "value": "blue28", + "title": "Blauw 28%" + }, { + "value": "red34", + "title": "Rood 34%" + }, { + "value": "blue40", + "title": "Blauw 40%" + }, { + "value": "green46", + "title": "Groen 46%" + }, { + "value": "orange52", + "title": "Oranje 52%" + }, { + "value": "red58", + "title": "Rood 58%" + }], + "type": "enum" + }, { + "id": "ticks_past_major", + "title": "Klikken voorbij de kleur", + "options": [{ + "value": "na", + "title": "NVT" + }, { + "value": "0", + "title": "0" + }, { + "value": "1", + "title": "1" + }, { + "value": "2", + "title": "2" + }, { + "value": "3", + "title": "3" + }, { + "value": "4", + "title": "4" + }, { + "value": "5", + "title": "5" + }], + "type": "enum" + }], + "score": [function(yellow_moved) { + if (yellow_moved === 'no') { + return 0 + } + if (yellow_moved === 'yes') { + return 20 + } + }, function(yellow_moved, dial_major_color, ticks_past_major) { + if (yellow_moved === 'no' && dial_major_color === 'na' && ticks_past_major === 'na') { + return 0 + } + if (yellow_moved === 'no' && dial_major_color === 'red10' && ticks_past_major === 'na') { + return new Error("Er moet of twee keer \"NVT\" of twee keer een waarde worden gekozen") + } + if (yellow_moved === 'no' && dial_major_color === 'orange16' && ticks_past_major === 'na') { + return new Error("Er moet of twee keer \"NVT\" of twee keer een waarde worden gekozen") + } + if (yellow_moved === 'no' && dial_major_color === 'green22' && ticks_past_major === 'na') { + return new Error("Er moet of twee keer \"NVT\" of twee keer een waarde worden gekozen") + } + if (yellow_moved === 'no' && dial_major_color === 'blue28' && ticks_past_major === 'na') { + return new Error("Er moet of twee keer \"NVT\" of twee keer een waarde worden gekozen") + } + if (yellow_moved === 'no' && dial_major_color === 'red34' && ticks_past_major === 'na') { + return new Error("Er moet of twee keer \"NVT\" of twee keer een waarde worden gekozen") + } + if (yellow_moved === 'no' && dial_major_color === 'blue40' && ticks_past_major === 'na') { + return new Error("Er moet of twee keer \"NVT\" of twee keer een waarde worden gekozen") + } + if (yellow_moved === 'no' && dial_major_color === 'green46' && ticks_past_major === 'na') { + return new Error("Er moet of twee keer \"NVT\" of twee keer een waarde worden gekozen") + } + if (yellow_moved === 'no' && dial_major_color === 'orange52' && ticks_past_major === 'na') { + return new Error("Er moet of twee keer \"NVT\" of twee keer een waarde worden gekozen") + } + if (yellow_moved === 'no' && dial_major_color === 'red58' && ticks_past_major === 'na') { + return new Error("Er moet of twee keer \"NVT\" of twee keer een waarde worden gekozen") + } + if (yellow_moved === 'no' && dial_major_color === 'na' && ticks_past_major === '0') { + return new Error("Er moet of twee keer \"NVT\" of twee keer een waarde worden gekozen") + } + if (yellow_moved === 'no' && dial_major_color === 'red10' && ticks_past_major === '0') { + return new Error("De wijzer blijft op \"NVT\" staan als het gele gedeelte niet geactiveerd is") + } + if (yellow_moved === 'no' && dial_major_color === 'orange16' && ticks_past_major === '0') { + return new Error("De wijzer blijft op \"NVT\" staan als het gele gedeelte niet geactiveerd is") + } + if (yellow_moved === 'no' && dial_major_color === 'green22' && ticks_past_major === '0') { + return new Error("De wijzer blijft op \"NVT\" staan als het gele gedeelte niet geactiveerd is") + } + if (yellow_moved === 'no' && dial_major_color === 'blue28' && ticks_past_major === '0') { + return new Error("De wijzer blijft op \"NVT\" staan als het gele gedeelte niet geactiveerd is") + } + if (yellow_moved === 'no' && dial_major_color === 'red34' && ticks_past_major === '0') { + return new Error("De wijzer blijft op \"NVT\" staan als het gele gedeelte niet geactiveerd is") + } + if (yellow_moved === 'no' && dial_major_color === 'blue40' && ticks_past_major === '0') { + return new Error("De wijzer blijft op \"NVT\" staan als het gele gedeelte niet geactiveerd is") + } + if (yellow_moved === 'no' && dial_major_color === 'green46' && ticks_past_major === '0') { + return new Error("De wijzer blijft op \"NVT\" staan als het gele gedeelte niet geactiveerd is") + } + if (yellow_moved === 'no' && dial_major_color === 'orange52' && ticks_past_major === '0') { + return new Error("De wijzer blijft op \"NVT\" staan als het gele gedeelte niet geactiveerd is") + } + if (yellow_moved === 'no' && dial_major_color === 'red58' && ticks_past_major === '0') { + return new Error("De wijzer blijft op \"NVT\" staan als het gele gedeelte niet geactiveerd is") + } + if (yellow_moved === 'no' && dial_major_color === 'na' && ticks_past_major === '1') { + return new Error("Er moet of twee keer \"NVT\" of twee keer een waarde worden gekozen") + } + if (yellow_moved === 'no' && dial_major_color === 'red10' && ticks_past_major === '1') { + return new Error("De wijzer blijft op \"NVT\" staan als het gele gedeelte niet geactiveerd is") + } + if (yellow_moved === 'no' && dial_major_color === 'orange16' && ticks_past_major === '1') { + return new Error("De wijzer blijft op \"NVT\" staan als het gele gedeelte niet geactiveerd is") + } + if (yellow_moved === 'no' && dial_major_color === 'green22' && ticks_past_major === '1') { + return new Error("De wijzer blijft op \"NVT\" staan als het gele gedeelte niet geactiveerd is") + } + if (yellow_moved === 'no' && dial_major_color === 'blue28' && ticks_past_major === '1') { + return new Error("De wijzer blijft op \"NVT\" staan als het gele gedeelte niet geactiveerd is") + } + if (yellow_moved === 'no' && dial_major_color === 'red34' && ticks_past_major === '1') { + return new Error("De wijzer blijft op \"NVT\" staan als het gele gedeelte niet geactiveerd is") + } + if (yellow_moved === 'no' && dial_major_color === 'blue40' && ticks_past_major === '1') { + return new Error("De wijzer blijft op \"NVT\" staan als het gele gedeelte niet geactiveerd is") + } + if (yellow_moved === 'no' && dial_major_color === 'green46' && ticks_past_major === '1') { + return new Error("De wijzer blijft op \"NVT\" staan als het gele gedeelte niet geactiveerd is") + } + if (yellow_moved === 'no' && dial_major_color === 'orange52' && ticks_past_major === '1') { + return new Error("De wijzer blijft op \"NVT\" staan als het gele gedeelte niet geactiveerd is") + } + if (yellow_moved === 'no' && dial_major_color === 'red58' && ticks_past_major === '1') { + return new Error("De wijzer blijft op \"NVT\" staan als het gele gedeelte niet geactiveerd is") + } + if (yellow_moved === 'no' && dial_major_color === 'na' && ticks_past_major === '2') { + return new Error("Er moet of twee keer \"NVT\" of twee keer een waarde worden gekozen") + } + if (yellow_moved === 'no' && dial_major_color === 'red10' && ticks_past_major === '2') { + return new Error("De wijzer blijft op \"NVT\" staan als het gele gedeelte niet geactiveerd is") + } + if (yellow_moved === 'no' && dial_major_color === 'orange16' && ticks_past_major === '2') { + return new Error("De wijzer blijft op \"NVT\" staan als het gele gedeelte niet geactiveerd is") + } + if (yellow_moved === 'no' && dial_major_color === 'green22' && ticks_past_major === '2') { + return new Error("De wijzer blijft op \"NVT\" staan als het gele gedeelte niet geactiveerd is") + } + if (yellow_moved === 'no' && dial_major_color === 'blue28' && ticks_past_major === '2') { + return new Error("De wijzer blijft op \"NVT\" staan als het gele gedeelte niet geactiveerd is") + } + if (yellow_moved === 'no' && dial_major_color === 'red34' && ticks_past_major === '2') { + return new Error("De wijzer blijft op \"NVT\" staan als het gele gedeelte niet geactiveerd is") + } + if (yellow_moved === 'no' && dial_major_color === 'blue40' && ticks_past_major === '2') { + return new Error("De wijzer blijft op \"NVT\" staan als het gele gedeelte niet geactiveerd is") + } + if (yellow_moved === 'no' && dial_major_color === 'green46' && ticks_past_major === '2') { + return new Error("De wijzer blijft op \"NVT\" staan als het gele gedeelte niet geactiveerd is") + } + if (yellow_moved === 'no' && dial_major_color === 'orange52' && ticks_past_major === '2') { + return new Error("De wijzer blijft op \"NVT\" staan als het gele gedeelte niet geactiveerd is") + } + if (yellow_moved === 'no' && dial_major_color === 'red58' && ticks_past_major === '2') { + return new Error("De wijzer blijft op \"NVT\" staan als het gele gedeelte niet geactiveerd is") + } + if (yellow_moved === 'no' && dial_major_color === 'na' && ticks_past_major === '3') { + return new Error("Er moet of twee keer \"NVT\" of twee keer een waarde worden gekozen") + } + if (yellow_moved === 'no' && dial_major_color === 'red10' && ticks_past_major === '3') { + return new Error("De wijzer blijft op \"NVT\" staan als het gele gedeelte niet geactiveerd is") + } + if (yellow_moved === 'no' && dial_major_color === 'orange16' && ticks_past_major === '3') { + return new Error("De wijzer blijft op \"NVT\" staan als het gele gedeelte niet geactiveerd is") + } + if (yellow_moved === 'no' && dial_major_color === 'green22' && ticks_past_major === '3') { + return new Error("De wijzer blijft op \"NVT\" staan als het gele gedeelte niet geactiveerd is") + } + if (yellow_moved === 'no' && dial_major_color === 'blue28' && ticks_past_major === '3') { + return new Error("De wijzer blijft op \"NVT\" staan als het gele gedeelte niet geactiveerd is") + } + if (yellow_moved === 'no' && dial_major_color === 'red34' && ticks_past_major === '3') { + return new Error("De wijzer blijft op \"NVT\" staan als het gele gedeelte niet geactiveerd is") + } + if (yellow_moved === 'no' && dial_major_color === 'blue40' && ticks_past_major === '3') { + return new Error("De wijzer blijft op \"NVT\" staan als het gele gedeelte niet geactiveerd is") + } + if (yellow_moved === 'no' && dial_major_color === 'green46' && ticks_past_major === '3') { + return new Error("De wijzer blijft op \"NVT\" staan als het gele gedeelte niet geactiveerd is") + } + if (yellow_moved === 'no' && dial_major_color === 'orange52' && ticks_past_major === '3') { + return new Error("De wijzer blijft op \"NVT\" staan als het gele gedeelte niet geactiveerd is") + } + if (yellow_moved === 'no' && dial_major_color === 'red58' && ticks_past_major === '3') { + return new Error("De wijzer blijft op \"NVT\" staan als het gele gedeelte niet geactiveerd is") + } + if (yellow_moved === 'no' && dial_major_color === 'na' && ticks_past_major === '4') { + return new Error("Er moet of twee keer \"NVT\" of twee keer een waarde worden gekozen") + } + if (yellow_moved === 'no' && dial_major_color === 'red10' && ticks_past_major === '4') { + return new Error("De wijzer blijft op \"NVT\" staan als het gele gedeelte niet geactiveerd is") + } + if (yellow_moved === 'no' && dial_major_color === 'orange16' && ticks_past_major === '4') { + return new Error("De wijzer blijft op \"NVT\" staan als het gele gedeelte niet geactiveerd is") + } + if (yellow_moved === 'no' && dial_major_color === 'green22' && ticks_past_major === '4') { + return new Error("De wijzer blijft op \"NVT\" staan als het gele gedeelte niet geactiveerd is") + } + if (yellow_moved === 'no' && dial_major_color === 'blue28' && ticks_past_major === '4') { + return new Error("De wijzer blijft op \"NVT\" staan als het gele gedeelte niet geactiveerd is") + } + if (yellow_moved === 'no' && dial_major_color === 'red34' && ticks_past_major === '4') { + return new Error("De wijzer blijft op \"NVT\" staan als het gele gedeelte niet geactiveerd is") + } + if (yellow_moved === 'no' && dial_major_color === 'blue40' && ticks_past_major === '4') { + return new Error("De wijzer blijft op \"NVT\" staan als het gele gedeelte niet geactiveerd is") + } + if (yellow_moved === 'no' && dial_major_color === 'green46' && ticks_past_major === '4') { + return new Error("De wijzer blijft op \"NVT\" staan als het gele gedeelte niet geactiveerd is") + } + if (yellow_moved === 'no' && dial_major_color === 'orange52' && ticks_past_major === '4') { + return new Error("De wijzer blijft op \"NVT\" staan als het gele gedeelte niet geactiveerd is") + } + if (yellow_moved === 'no' && dial_major_color === 'red58' && ticks_past_major === '4') { + return new Error("De wijzer blijft op \"NVT\" staan als het gele gedeelte niet geactiveerd is") + } + if (yellow_moved === 'no' && dial_major_color === 'na' && ticks_past_major === '5') { + return new Error("Er moet of twee keer \"NVT\" of twee keer een waarde worden gekozen") + } + if (yellow_moved === 'no' && dial_major_color === 'red10' && ticks_past_major === '5') { + return new Error("De wijzer blijft op \"NVT\" staan als het gele gedeelte niet geactiveerd is") + } + if (yellow_moved === 'no' && dial_major_color === 'orange16' && ticks_past_major === '5') { + return new Error("De wijzer blijft op \"NVT\" staan als het gele gedeelte niet geactiveerd is") + } + if (yellow_moved === 'no' && dial_major_color === 'green22' && ticks_past_major === '5') { + return new Error("De wijzer blijft op \"NVT\" staan als het gele gedeelte niet geactiveerd is") + } + if (yellow_moved === 'no' && dial_major_color === 'blue28' && ticks_past_major === '5') { + return new Error("De wijzer blijft op \"NVT\" staan als het gele gedeelte niet geactiveerd is") + } + if (yellow_moved === 'no' && dial_major_color === 'red34' && ticks_past_major === '5') { + return new Error("De wijzer blijft op \"NVT\" staan als het gele gedeelte niet geactiveerd is") + } + if (yellow_moved === 'no' && dial_major_color === 'blue40' && ticks_past_major === '5') { + return new Error("De wijzer blijft op \"NVT\" staan als het gele gedeelte niet geactiveerd is") + } + if (yellow_moved === 'no' && dial_major_color === 'green46' && ticks_past_major === '5') { + return new Error("De wijzer blijft op \"NVT\" staan als het gele gedeelte niet geactiveerd is") + } + if (yellow_moved === 'no' && dial_major_color === 'orange52' && ticks_past_major === '5') { + return new Error("De wijzer blijft op \"NVT\" staan als het gele gedeelte niet geactiveerd is") + } + if (yellow_moved === 'no' && dial_major_color === 'red58' && ticks_past_major === '5') { + return new Error("De wijzer blijft op \"NVT\" staan als het gele gedeelte niet geactiveerd is") + } + if (yellow_moved === 'yes' && dial_major_color === 'na' && ticks_past_major === 'na') { + return 0 + } + if (yellow_moved === 'yes' && dial_major_color === 'red10' && ticks_past_major === 'na') { + return new Error("Er moet of twee keer \"NVT\" of twee keer een waarde worden gekozen") + } + if (yellow_moved === 'yes' && dial_major_color === 'orange16' && ticks_past_major === 'na') { + return new Error("Er moet of twee keer \"NVT\" of twee keer een waarde worden gekozen") + } + if (yellow_moved === 'yes' && dial_major_color === 'green22' && ticks_past_major === 'na') { + return new Error("Er moet of twee keer \"NVT\" of twee keer een waarde worden gekozen") + } + if (yellow_moved === 'yes' && dial_major_color === 'blue28' && ticks_past_major === 'na') { + return new Error("Er moet of twee keer \"NVT\" of twee keer een waarde worden gekozen") + } + if (yellow_moved === 'yes' && dial_major_color === 'red34' && ticks_past_major === 'na') { + return new Error("Er moet of twee keer \"NVT\" of twee keer een waarde worden gekozen") + } + if (yellow_moved === 'yes' && dial_major_color === 'blue40' && ticks_past_major === 'na') { + return new Error("Er moet of twee keer \"NVT\" of twee keer een waarde worden gekozen") + } + if (yellow_moved === 'yes' && dial_major_color === 'green46' && ticks_past_major === 'na') { + return new Error("Er moet of twee keer \"NVT\" of twee keer een waarde worden gekozen") + } + if (yellow_moved === 'yes' && dial_major_color === 'orange52' && ticks_past_major === 'na') { + return new Error("Er moet of twee keer \"NVT\" of twee keer een waarde worden gekozen") + } + if (yellow_moved === 'yes' && dial_major_color === 'red58' && ticks_past_major === 'na') { + return new Error("Er moet of twee keer \"NVT\" of twee keer een waarde worden gekozen") + } + if (yellow_moved === 'yes' && dial_major_color === 'na' && ticks_past_major === '0') { + return new Error("Er moet of twee keer \"NVT\" of twee keer een waarde worden gekozen") + } + if (yellow_moved === 'yes' && dial_major_color === 'red10' && ticks_past_major === '0') { + return 0.1 + } + if (yellow_moved === 'yes' && dial_major_color === 'orange16' && ticks_past_major === '0') { + return 0.16 + } + if (yellow_moved === 'yes' && dial_major_color === 'green22' && ticks_past_major === '0') { + return 0.22 + } + if (yellow_moved === 'yes' && dial_major_color === 'blue28' && ticks_past_major === '0') { + return 0.28 + } + if (yellow_moved === 'yes' && dial_major_color === 'red34' && ticks_past_major === '0') { + return 0.34 + } + if (yellow_moved === 'yes' && dial_major_color === 'blue40' && ticks_past_major === '0') { + return 0.4 + } + if (yellow_moved === 'yes' && dial_major_color === 'green46' && ticks_past_major === '0') { + return 0.46 + } + if (yellow_moved === 'yes' && dial_major_color === 'orange52' && ticks_past_major === '0') { + return 0.52 + } + if (yellow_moved === 'yes' && dial_major_color === 'red58' && ticks_past_major === '0') { + return 0.58 + } + if (yellow_moved === 'yes' && dial_major_color === 'na' && ticks_past_major === '1') { + return new Error("Er moet of twee keer \"NVT\" of twee keer een waarde worden gekozen") + } + if (yellow_moved === 'yes' && dial_major_color === 'red10' && ticks_past_major === '1') { + return 0.11 + } + if (yellow_moved === 'yes' && dial_major_color === 'orange16' && ticks_past_major === '1') { + return 0.17 + } + if (yellow_moved === 'yes' && dial_major_color === 'green22' && ticks_past_major === '1') { + return 0.23 + } + if (yellow_moved === 'yes' && dial_major_color === 'blue28' && ticks_past_major === '1') { + return 0.29 + } + if (yellow_moved === 'yes' && dial_major_color === 'red34' && ticks_past_major === '1') { + return 0.35 + } + if (yellow_moved === 'yes' && dial_major_color === 'blue40' && ticks_past_major === '1') { + return 0.41 + } + if (yellow_moved === 'yes' && dial_major_color === 'green46' && ticks_past_major === '1') { + return 0.47 + } + if (yellow_moved === 'yes' && dial_major_color === 'orange52' && ticks_past_major === '1') { + return 0.53 + } + if (yellow_moved === 'yes' && dial_major_color === 'red58' && ticks_past_major === '1') { + return 0.58 + } + if (yellow_moved === 'yes' && dial_major_color === 'na' && ticks_past_major === '2') { + return new Error("Er moet of twee keer \"NVT\" of twee keer een waarde worden gekozen") + } + if (yellow_moved === 'yes' && dial_major_color === 'red10' && ticks_past_major === '2') { + return 0.12 + } + if (yellow_moved === 'yes' && dial_major_color === 'orange16' && ticks_past_major === '2') { + return 0.18 + } + if (yellow_moved === 'yes' && dial_major_color === 'green22' && ticks_past_major === '2') { + return 0.24 + } + if (yellow_moved === 'yes' && dial_major_color === 'blue28' && ticks_past_major === '2') { + return 0.3 + } + if (yellow_moved === 'yes' && dial_major_color === 'red34' && ticks_past_major === '2') { + return 0.36 + } + if (yellow_moved === 'yes' && dial_major_color === 'blue40' && ticks_past_major === '2') { + return 0.42 + } + if (yellow_moved === 'yes' && dial_major_color === 'green46' && ticks_past_major === '2') { + return 0.48 + } + if (yellow_moved === 'yes' && dial_major_color === 'orange52' && ticks_past_major === '2') { + return 0.54 + } + if (yellow_moved === 'yes' && dial_major_color === 'red58' && ticks_past_major === '2') { + return new Error("De wijzer kan niet zover draaien") + } + if (yellow_moved === 'yes' && dial_major_color === 'na' && ticks_past_major === '3') { + return new Error("Er moet of twee keer \"NVT\" of twee keer een waarde worden gekozen") + } + if (yellow_moved === 'yes' && dial_major_color === 'red10' && ticks_past_major === '3') { + return 0.13 + } + if (yellow_moved === 'yes' && dial_major_color === 'orange16' && ticks_past_major === '3') { + return 0.19 + } + if (yellow_moved === 'yes' && dial_major_color === 'green22' && ticks_past_major === '3') { + return 0.25 + } + if (yellow_moved === 'yes' && dial_major_color === 'blue28' && ticks_past_major === '3') { + return 0.31 + } + if (yellow_moved === 'yes' && dial_major_color === 'red34' && ticks_past_major === '3') { + return 0.37 + } + if (yellow_moved === 'yes' && dial_major_color === 'blue40' && ticks_past_major === '3') { + return 0.43 + } + if (yellow_moved === 'yes' && dial_major_color === 'green46' && ticks_past_major === '3') { + return 0.49 + } + if (yellow_moved === 'yes' && dial_major_color === 'orange52' && ticks_past_major === '3') { + return 0.55 + } + if (yellow_moved === 'yes' && dial_major_color === 'red58' && ticks_past_major === '3') { + return new Error("De wijzer kan niet zover draaien") + } + if (yellow_moved === 'yes' && dial_major_color === 'na' && ticks_past_major === '4') { + return new Error("Er moet of twee keer \"NVT\" of twee keer een waarde worden gekozen") + } + if (yellow_moved === 'yes' && dial_major_color === 'red10' && ticks_past_major === '4') { + return 0.14 + } + if (yellow_moved === 'yes' && dial_major_color === 'orange16' && ticks_past_major === '4') { + return 0.2 + } + if (yellow_moved === 'yes' && dial_major_color === 'green22' && ticks_past_major === '4') { + return 0.26 + } + if (yellow_moved === 'yes' && dial_major_color === 'blue28' && ticks_past_major === '4') { + return 0.32 + } + if (yellow_moved === 'yes' && dial_major_color === 'red34' && ticks_past_major === '4') { + return 0.38 + } + if (yellow_moved === 'yes' && dial_major_color === 'blue40' && ticks_past_major === '4') { + return 0.44 + } + if (yellow_moved === 'yes' && dial_major_color === 'green46' && ticks_past_major === '4') { + return 0.5 + } + if (yellow_moved === 'yes' && dial_major_color === 'orange52' && ticks_past_major === '4') { + return 0.56 + } + if (yellow_moved === 'yes' && dial_major_color === 'red58' && ticks_past_major === '4') { + return new Error("De wijzer kan niet zover draaien") + } + if (yellow_moved === 'yes' && dial_major_color === 'na' && ticks_past_major === '5') { + return new Error("Er moet of twee keer \"NVT\" of twee keer een waarde worden gekozen") + } + if (yellow_moved === 'yes' && dial_major_color === 'red10' && ticks_past_major === '5') { + return 0.15 + } + if (yellow_moved === 'yes' && dial_major_color === 'orange16' && ticks_past_major === '5') { + return 0.21 + } + if (yellow_moved === 'yes' && dial_major_color === 'green22' && ticks_past_major === '5') { + return 0.27 + } + if (yellow_moved === 'yes' && dial_major_color === 'blue28' && ticks_past_major === '5') { + return 0.33 + } + if (yellow_moved === 'yes' && dial_major_color === 'red34' && ticks_past_major === '5') { + return 0.39 + } + if (yellow_moved === 'yes' && dial_major_color === 'blue40' && ticks_past_major === '5') { + return 0.45 + } + if (yellow_moved === 'yes' && dial_major_color === 'green46' && ticks_past_major === '5') { + return 0.51 + } + if (yellow_moved === 'yes' && dial_major_color === 'orange52' && ticks_past_major === '5') { + return 0.57 + } + if (yellow_moved === 'yes' && dial_major_color === 'red58' && ticks_past_major === '5') { + return new Error("De wijzer kan niet zover draaien") + } + }] + }, { + "title": "Flexibiliteit", + "description": "De zichtbare situatie aan het einde van de wedstrijd:
      • Het model is 90⁰ gedraaid tegen de richting van de klok in ten opzichte van de beginpositie.
      Vereiste methode en beperkingen:
      • Geen.
      ", + "objectives": [{ + "id": "model_rotated", + "title": "Model is 90 graden tegen de klok in gedraaid", + "type": "yesno" + }], + "score": [function(model_rotated) { + if (model_rotated === 'no') { + return 0 + } + if (model_rotated === 'yes') { + return 15 + } + }] + }, { + "title": "Strafpunten", + "description": "training-desc", + "objectives": [{ + "id": "penalties_objective", + "title": "Robot, rommel of uitvouwstrafpunten", + "options": [{ + "value": "0", + "title": "0" + }, { + "value": "1", + "title": "1" + }, { + "value": "2", + "title": "2" + }, { + "value": "3", + "title": "3" + }, { + "value": "4", + "title": "4" + }, { + "value": "5", + "title": "5" + }, { + "value": "6", + "title": "6" + }, { + "value": "7", + "title": "7" + }, { + "value": "8", + "title": "8" + }], + "type": "enum" + }], + "score": [function(penalties_objective) { + if (penalties_objective === '0') { + return 0 + } + if (penalties_objective === '1') { + return -10 + } + if (penalties_objective === '2') { + return -20 + } + if (penalties_objective === '3') { + return -30 + } + if (penalties_objective === '4') { + return -40 + } + if (penalties_objective === '5') { + return -50 + } + if (penalties_objective === '6') { + return -60 + } + if (penalties_objective === '7') { + return -70 + } + if (penalties_objective === '8') { + return -80 + } + }] + }] +} diff --git a/challenges/pdf/2012.pdf b/challenges/pdf/2012.pdf index abc71c79eebfa570328c18a045ae277c152ead68..ba2523a5c28f0d50771ff4fbff1cdaf0d8099843 100644 GIT binary patch delta 21 ccmey~$@IOGX~H!QBNIa-BTEyLjrSy50Ad6PI{*Lx delta 21 ccmey~$@IOGX~H!QLrWtA0}BJAjrSy50AcP3H2?qr diff --git a/challenges/pdf/2014.pdf b/challenges/pdf/2014.pdf index 1267bd7c2c9cf2bb7d3e4cd7e5050a20d7c2bb20..297296b95c9e0ddfeeb9c6351bd3528eddfb6689 100644 GIT binary patch delta 20 ccmdn8oN3E)rU}>BjZ6%Uj4Vwy-n~8v09OkLhX4Qo delta 20 ccmdn8oN3E)rU}>B4K0lf3@i*b-n~8v09OMDg8%>k diff --git a/challenges/pdf/2014_nl_NL-no-enum.pdf b/challenges/pdf/2014_nl_NL-no-enum.pdf new file mode 100644 index 0000000000000000000000000000000000000000..2770b642473a37e34956647f9e5331dd066ac1b0 GIT binary patch literal 38085 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

      TSgnKjq1P_cpl-)^WWY)-q*? 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 literal 0 HcmV?d00001 diff --git a/challenges/pdf/2014_nl_NL.pdf b/challenges/pdf/2014_nl_NL.pdf index c09291558a4e77ba76d338868846016683c91e19..6cb16ad8295c7425d28ea167092a07abfdb6e484 100644 GIT binary patch delta 9699 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 delta 9689 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 U$b67>hyo@6fW^clr!J56KkwL8kpKVy From be52e3c39c60a84977e14d3d78fa7250ebad0609 Mon Sep 17 00:00:00 2001 From: rikkertkoppes Date: Mon, 24 Nov 2014 12:34:56 +0100 Subject: [PATCH 13/29] Updated challenge to dutch version with numbers as enums --- src/js/services/ng-challenge.js | 972 +++++++------------------------- 1 file changed, 212 insertions(+), 760 deletions(-) diff --git a/src/js/services/ng-challenge.js b/src/js/services/ng-challenge.js index 82e385b7..b2826edf 100644 --- a/src/js/services/ng-challenge.js +++ b/src/js/services/ng-challenge.js @@ -8,621 +8,20 @@ define('services/ng-challenge',[ '$http','$settings', function($http,$settings) { var mission; - var fallBackChallenge = 'challenge/2014_nl_NL'; - - var field = { - "title": "Senior Solutions", - "missions": [{ - "title": "Flexibility", - "description": "Robot gets yellow loops to Base.", - "objectives": [{ - "id": "yellowloops", - "title": "Yellow Loops in Base", - "type": "number", - "min": "0", - "max": "2" - }], - "score": [ - function(yellowloops) { - if (yellowloops === '0') { - return 0 - } - if (yellowloops === '1') { - return 20 - } - if (yellowloops === '2') { - return 40 - } - } - ] - }, { - "title": "Medicine", - "description": "The bottles are arranged randomly before the start of each match (See Field Setup). Robot gets the green medicine bottle to Base without disturbing orange ones.", - "objectives": [{ - "id": "meds", - "title": "Green in Base, Orange Not Moved", - "type": "yesno" - }], - "score": [ - function(meds) { - if (meds === 'no') { - return 0 - } - if (meds === 'yes') { - return 25 - } - } - ] - }, { - "title": "Service Animals", - "description": "Robot applies force to gray disc, causing dog with phone to move toward Base.", - "objectives": [{ - "id": "dog", - "title": "Dog in Base", - "type": "yesno" - }], - "score": [ - function(dog) { - if (dog === 'no') { - return 0 - } - if (dog === 'yes') { - return 20 - } - } - ] - }, { - "title": "Wood Working", - "description": "Robot gets the chair to Base. You fix the chair by hand. Robot brings the chair to the table.", - "objectives": [{ - "id": "chairbase", - "title": "Chair Repaired and in Base", - "type": "yesno" - }, { - "id": "chairtable", - "title": "Chair Repaired and under Table", - "type": "yesno" - }], - "score": [ - function(chairbase, chairtable) { - if (chairbase === 'no' && chairtable === 'no') { - return 0 - } - if (chairbase === 'no' && chairtable === 'yes') { - return 25 - } - if (chairbase === 'yes' && chairtable === 'no') { - return 15 - } - if (chairbase === 'yes' && chairtable === 'yes') { - return new Error("Chair cannot be in base AND under the table.") - } - } - ] - }, { - "title": "Video Call", - "description": "Robot gets the flags to rise.", - "objectives": [{ - "id": "flags", - "title": "Flags Fully Upright", - "type": "number", - "min": "0", - "max": "2" - }], - "score": [ - function(flags) { - if (flags === '0') { - return 0 - } - if (flags === '1') { - return 20 - } - if (flags === '2') { - return 40 - } - } - ] - }, { - "title": "Quilts", - "description": "Robot adds squares to quilts.", - "objectives": [{ - "id": "quiltsblue", - "title": "Blue Squares Touch Target", - "type": "number", - "min": "0", - "max": "2" - }, { - "id": "quiltsorange", - "title": "Orange Squares Touch Target", - "type": "number", - "min": "0", - "max": "2" - }], - "score": [ - function(quiltsblue) { - if (quiltsblue === '0') { - return 0 - } - if (quiltsblue === '1') { - return 15 - } - if (quiltsblue === '2') { - return 30 - } - }, - function(quiltsorange) { - if (quiltsorange === '0') { - return 0 - } - if (quiltsorange === '1') { - return 30 - } - if (quiltsorange === '2') { - return 60 - } - } - ] - }, { - "title": "Similarity Recognition and Cooperation", - "description": "Robot aligns your pointer with the other team’s pointer.", - "objectives": [{ - "id": "coop", - "title": "Dials on Both Fields are Parallel", - "type": "yesno" - }], - "score": [ - function(coop) { - if (coop === 'no') { - return 0 - } - if (coop === 'yes') { - return 45 - } - } - ] - }, { - "title": "Ball Game", - "description": "Both teams get points for the total number of balls on the racks at the end of the match, but only one team gets points when their color is at the center.", - "objectives": [{ - "id": "ballcount", - "title": "Balls on Rack", - "type": "number", - "min": "0", - "max": "7" - }, { - "id": "ballmiddle", - "title": "Middle Ball", - "options": [{ - "value": "your", - "title": "Yours" - }, { - "value": "them", - "title": "Theirs" - }, { - "value": "none", - "title": "Neither" - }], - "type": "enum" - }], - "score": [ - function(ballmiddle, ballcount) { - if (ballmiddle === 'your' && ballcount === '0') { - return new Error("When no balls are left, the middle ball must be 'Neither'.") - } - if (ballmiddle === 'your' && ballcount === '1') { - return 70 - } - if (ballmiddle === 'your' && ballcount === '2') { - return 80 - } - if (ballmiddle === 'your' && ballcount === '3') { - return 90 - } - if (ballmiddle === 'your' && ballcount === '4') { - return 100 - } - if (ballmiddle === 'your' && ballcount === '5') { - return 110 - } - if (ballmiddle === 'your' && ballcount === '6') { - return 120 - } - if (ballmiddle === 'your' && ballcount === '7') { - return new Error("When all balls are left, the middle ball must be 'Neither'.") - } - if (ballmiddle === 'them' && ballcount === '0') { - return new Error("When no balls are left, the middle ball must be 'Neither'.") - } - if (ballmiddle === 'them' && ballcount === '1') { - return 10 - } - if (ballmiddle === 'them' && ballcount === '2') { - return 20 - } - if (ballmiddle === 'them' && ballcount === '3') { - return 30 - } - if (ballmiddle === 'them' && ballcount === '4') { - return 40 - } - if (ballmiddle === 'them' && ballcount === '5') { - return 50 - } - if (ballmiddle === 'them' && ballcount === '6') { - return 60 - } - if (ballmiddle === 'them' && ballcount === '7') { - return new Error("When all balls are left, the middle ball must be 'Neither'.") - } - if (ballmiddle === 'none' && ballcount === '0') { - return 0 - } - if (ballmiddle === 'none' && ballcount === '1') { - return new Error("When some, but not all, balls are left, the middle ball cannot be 'Neither'.") - } - if (ballmiddle === 'none' && ballcount === '2') { - return new Error("When some, but not all, balls are left, the middle ball cannot be 'Neither'.") - } - if (ballmiddle === 'none' && ballcount === '3') { - return new Error("When some, but not all, balls are left, the middle ball cannot be 'Neither'.") - } - if (ballmiddle === 'none' && ballcount === '4') { - return new Error("When some, but not all, balls are left, the middle ball cannot be 'Neither'.") - } - if (ballmiddle === 'none' && ballcount === '5') { - return new Error("When some, but not all, balls are left, the middle ball cannot be 'Neither'.") - } - if (ballmiddle === 'none' && ballcount === '6') { - return new Error("When some, but not all, balls are left, the middle ball cannot be 'Neither'.") - } - if (ballmiddle === 'none' && ballcount === '7') { - return 70 - } - } - ] - }, { - "title": "Gardening", - "description": "Robot adds to the garden.", - "objectives": [{ - "id": "plants", - "title": "Base of Plants Touch Target", - "type": "yesno" - }], - "score": [ - function(plants) { - if (plants === 'no') { - return 0 - } - if (plants === 'yes') { - return 25 - } - } - ] - }, { - "title": "Stove", - "description": "Robot gets all burners to show black.", - "objectives": [{ - "id": "burners", - "title": "All Burners are Black", - "type": "yesno" - }], - "score": [ - function(burners) { - if (burners === 'no') { - return 0 - } - if (burners === 'yes') { - return 25 - } - } - ] - }, { - "title": "Bowling", - "description": "Robot sends balls to knock pins down. If the pins are not all down after the first try using a yellow ball, the referee returns that ball to Base for a second try (this can only happen once per match).", - "objectives": [{ - "id": "pins", - "title": "Pins Hit", - "type": "number", - "min": "0", - "max": "6" - }], - "score": [ - function(pins) { - if (pins === '0') { - return 0 - } - if (pins === '1') { - return 7 - } - if (pins === '2') { - return 14 - } - if (pins === '3') { - return 21 - } - if (pins === '4') { - return 28 - } - if (pins === '5') { - return 35 - } - if (pins === '6') { - return 60 - } - } - ] - }, { - "title": "Transitions", - "description": "Robot gets onto the center platform and is there when the match ends.", - "objectives": [{ - "id": "platslant", - "title": "Robot Only Touches Slanted Platform", - "type": "yesno" - }, { - "id": "platbalan", - "title": "Robot Only Touches Balanced Platform", - "type": "yesno" - }, { - "id": "platclear", - "title": "Platform Only Touches Robot and Mat", - "type": "yesno" - }], - "score": [ - function(platslant, platbalan, platclear) { - if (platslant === 'no' && platbalan === 'no' && platclear === 'no') { - return 0 - } - if (platslant === 'no' && platbalan === 'no' && platclear === 'yes') { - return 0 - } - if (platslant === 'no' && platbalan === 'yes' && platclear === 'no') { - return 0 - } - if (platslant === 'no' && platbalan === 'yes' && platclear === 'yes') { - return 65 - } - if (platslant === 'yes' && platbalan === 'no' && platclear === 'no') { - return 0 - } - if (platslant === 'yes' && platbalan === 'no' && platclear === 'yes') { - return 45 - } - if (platslant === 'yes' && platbalan === 'yes' && platclear === 'no') { - return new Error("Platform cannot be slanted AND balanced.") - } - if (platslant === 'yes' && platbalan === 'yes' && platclear === 'yes') { - return new Error("Platform cannot be slanted AND balanced.") - } - } - ] - }, { - "title": "Strength Exercise", - "description": "Robot lifts the west bar to make the weight rise.", - "objectives": [{ - "id": "strength", - "title": "Weight raised", - "options": [{ - "value": "lo", - "title": "Low" - }, { - "value": "hi", - "title": "High" - }, { - "value": "no", - "title": "Not Done" - }], - "type": "enum" - }], - "score": [ - function(strength) { - if (strength === 'no') { - return 0 - } - if (strength === 'lo') { - return 15 - } - if (strength === 'hi') { - return 25 - } - } - ] - }, { - "title": "Cardio Training", - "description": "Robot turns the pinwheel 90° at a time.", - "objectives": [{ - "id": "dialbig", - "title": "Dial Big Step", - "type": "number", - "min": "1", - "max": "9" - }, { - "id": "dialsmall", - "title": "Dial Small Step", - "type": "number", - "min": "0", - "max": "5" - }], - "score": [ - function(dialbig, dialsmall) { - if (dialbig === '1' && dialsmall === '0') { - return -60 - } - if (dialbig === '1' && dialsmall === '1') { - return -55 - } - if (dialbig === '1' && dialsmall === '2') { - return -50 - } - if (dialbig === '1' && dialsmall === '3') { - return -45 - } - if (dialbig === '1' && dialsmall === '4') { - return -40 - } - if (dialbig === '1' && dialsmall === '5') { - return -35 - } - if (dialbig === '2' && dialsmall === '0') { - return -30 - } - if (dialbig === '2' && dialsmall === '1') { - return -25 - } - if (dialbig === '2' && dialsmall === '2') { - return -20 - } - if (dialbig === '2' && dialsmall === '3') { - return -15 - } - if (dialbig === '2' && dialsmall === '4') { - return -10 - } - if (dialbig === '2' && dialsmall === '5') { - return -5 - } - if (dialbig === '3' && dialsmall === '0') { - return 0 - } - if (dialbig === '3' && dialsmall === '1') { - return 5 - } - if (dialbig === '3' && dialsmall === '2') { - return 10 - } - if (dialbig === '3' && dialsmall === '3') { - return 15 - } - if (dialbig === '3' && dialsmall === '4') { - return 20 - } - if (dialbig === '3' && dialsmall === '5') { - return 25 - } - if (dialbig === '4' && dialsmall === '0') { - return 30 - } - if (dialbig === '4' && dialsmall === '1') { - return 35 - } - if (dialbig === '4' && dialsmall === '2') { - return 40 - } - if (dialbig === '4' && dialsmall === '3') { - return 45 - } - if (dialbig === '4' && dialsmall === '4') { - return 50 - } - if (dialbig === '4' && dialsmall === '5') { - return 55 - } - if (dialbig === '5' && dialsmall === '0') { - return 60 - } - if (dialbig === '5' && dialsmall === '1') { - return 63 - } - if (dialbig === '5' && dialsmall === '2') { - return 66 - } - if (dialbig === '5' && dialsmall === '3') { - return 69 - } - if (dialbig === '5' && dialsmall === '4') { - return 72 - } - if (dialbig === '5' && dialsmall === '5') { - return 75 - } - if (dialbig === '6' && dialsmall === '0') { - return 78 - } - if (dialbig === '6' && dialsmall === '1') { - return 91 - } - if (dialbig === '6' && dialsmall === '2') { - return 94 - } - if (dialbig === '6' && dialsmall === '3') { - return 97 - } - if (dialbig === '6' && dialsmall === '4') { - return 100 - } - if (dialbig === '6' && dialsmall === '5') { - return 103 - } - if (dialbig === '7' && dialsmall === '0') { - return 106 - } - if (dialbig === '7' && dialsmall === '1') { - return 107 - } - if (dialbig === '7' && dialsmall === '2') { - return 108 - } - if (dialbig === '7' && dialsmall === '3') { - return 109 - } - if (dialbig === '7' && dialsmall === '4') { - return 110 - } - if (dialbig === '7' && dialsmall === '5') { - return 111 - } - if (dialbig === '8' && dialsmall === '0') { - return 112 - } - if (dialbig === '8' && dialsmall === '1') { - return 113 - } - if (dialbig === '8' && dialsmall === '2') { - return 114 - } - if (dialbig === '8' && dialsmall === '3') { - return 115 - } - if (dialbig === '8' && dialsmall === '4') { - return 116 - } - if (dialbig === '8' && dialsmall === '5') { - return 117 - } - if (dialbig === '9' && dialsmall === '0') { - return 118 - } - if (dialbig === '9' && dialsmall === '1') { - return new Error("When Big Dial is on 9, Small Dial must be on 0.") - } - if (dialbig === '9' && dialsmall === '2') { - return new Error("When Big Dial is on 9, Small Dial must be on 0.") - } - if (dialbig === '9' && dialsmall === '3') { - return new Error("When Big Dial is on 9, Small Dial must be on 0.") - } - if (dialbig === '9' && dialsmall === '4') { - return new Error("When Big Dial is on 9, Small Dial must be on 0.") - } - if (dialbig === '9' && dialsmall === '5') { - return new Error("When Big Dial is on 9, Small Dial must be on 0.") - } - } - ] - }] - }; + var fallBackChallenge = 'challenge/2014_nl_NL-no-enum'; var field = { "title": "World Class", "missions": [{ "title": "Reverse Engineering", - "description": "Todo: Kenny", + "description": "De zichtbare situatie aan het einde van de wedstrijd:

      • Jullie mand is in de basis.
      • Het model raakt de witte cirkel rond het projectonderwijs-model aan.
      • Jullie hebben een model gemaakt ‘identiek’ aan het model dat het andere team in jullie mand heeft gedaan. De verbindingen tussen de elementen moeten hetzelfde zijn, maar elementen mogen wel ‘gedraaid’ zitten.
      • Het model is in de basis.
      Vereiste methode en beperkingen:
      • Geen.
      ", "objectives": [{ "id": "basket", - "title": "Basket in Base", + "title": "Mand in de basis", "type": "yesno" }, { "id": "identical", - "title": "Your model is in Base and is identical", + "title": "Jullie model ligt in de basis en is ‘identiek’", "type": "yesno" }], "score": [function(basket, identical) { @@ -640,11 +39,11 @@ define('services/ng-challenge',[ } }] }, { - "title": "Opening Doors", - "description": "Todo: Kenny", + "title": "Deuren openen", + "description": "De zichtbare situatie aan het einde van de wedstrijd:
      • De deur moet ver genoeg geopend zijn zodat de scheidsrechter dit kan zien.
      Vereiste methode en beperkingen:
      • De hendel moet omlaag gedrukt zijn.
      ", "objectives": [{ "id": "dooropen", - "title": "Door opened by pushing handle down", + "title": "Deur geopend door de hendel omlaag te drukken", "type": "yesno" }], "score": [function(dooropen) { @@ -656,14 +55,40 @@ define('services/ng-challenge',[ } }] }, { - "title": "Project-Based Learning", - "description": "Todo: Kenny", + "title": "Projectonderwijs", + "description": "De zichtbare situatie aan het einde van de wedstrijd:
      • De lussen (welke symbool staan voor kennis en vaardigheden) hangen aan de weegschaal zoals getoond.
      Vereiste methode en beperkingen:
      • Geen.
      ", "objectives": [{ "id": "loops", - "title": "Loops on scale", - "type": "number", - "min": "0", - "max": "8" + "title": "Lussen aan de weegschaal", + "options": [{ + "value": "0", + "title": "0" + }, { + "value": "1", + "title": "1" + }, { + "value": "2", + "title": "2" + }, { + "value": "3", + "title": "3" + }, { + "value": "4", + "title": "4" + }, { + "value": "5", + "title": "5" + }, { + "value": "6", + "title": "6" + }, { + "value": "7", + "title": "7" + }, { + "value": "8", + "title": "8" + }], + "type": "enum" }], "score": [function(loops) { if (loops === '0') { @@ -695,15 +120,15 @@ define('services/ng-challenge',[ } }] }, { - "title": "Apprenticeship", - "description": "Todo: Kenny", + "title": "Stagelopen", + "description": "De zichtbare situatie aan het einde van de wedstrijd:
      • De LEGO-poppetjes zijn beiden verbonden (op een manier naar keuze) aan een model dat jullie ontwerpen en meenemen. Dit model stelt een vaardigheid, prestatie, carrière of hobby voor dat een speciale betekenis voor jullie team heeft.
      • Het model raakt de witte cirkel rond het projectonderwijs-model aan.
      • Het model is niet in de basis.
      • Het vastmaken van missiemodellen is normaal niet toegestaan vanwege regel 39.4, deze missie is daar een uitzondering op.
      • Het eigen model mag simpel, of complex zijn, het mag primitief of realistisch zijn, de keuze is aan jullie. De keuze wat voor model jullie bouwen, heeft geen invloed op de score.
      Vereiste methode en beperkingen:
      • Geen.
      ", "objectives": [{ "id": "modelshown", - "title": "Model presented to Referee", + "title": "Model getoond aan de scheidsrechter", "type": "yesno" }, { "id": "touchingcicrle", - "title": "Touching circle, not in Base, people Bound", + "title": "Raakt de cirkel, niet in de basis en poppetjes verbonden", "type": "yesno" }], "score": [function(modelshown, touchingcicrle) { @@ -711,7 +136,7 @@ define('services/ng-challenge',[ return 0 } if (modelshown === 'no' && touchingcicrle === 'yes') { - return new Error("Model must have been presented for it to be touching the circle") + return new Error("Model moet getoond zijn voordat het de cirkel kan aanraken") } if (modelshown === 'yes' && touchingcicrle === 'no') { return 20 @@ -721,15 +146,15 @@ define('services/ng-challenge',[ } }] }, { - "title": "Search Engine", - "description": "Todo: Kenny", + "title": "Zoekmachine", + "description": "De zichtbare situatie aan het einde van de wedstrijd:
      • Het kleurenwiel heeft minimaal een keer gedraaid.
      • Als één kleur verschijnt in het witte frame, dan raakt de lus van de zichtbare kleur het model niet meer aan.
      • Als twee kleuren verschijnen in het witte venster, dan is de lus van de kleur die niet zichtbaar is in het venster, de lus die het model niet meer raakt.
      • Beide lussen die niet verwijderd dienen te worden raken via ‘hun’ gaten het model aan.
      Vereiste methode en beperkingen:
      • Alleen de beweging van de schuif heeft het kleurenwiel in beweging gebracht.
      ", "objectives": [{ "id": "wheelspin", - "title": "Only Slider caused wheel to spin 1+ times", + "title": "Alleen de schuif heeft het wiel 1+ keer rondgedraaid", "type": "yesno" }, { "id": "searchloop", - "title": "Only correct loop removed", + "title": "Alleen de juiste lus is verwijderd", "type": "yesno" }], "score": [function(wheelspin, searchloop) { @@ -747,15 +172,15 @@ define('services/ng-challenge',[ } }] }, { - "title": "Sports", - "description": "Todo: Kenny", + "title": "Sport", + "description": "De zichtbare situatie aan het einde van de wedstrijd:
      • De bal raakt de mat in het doel.
      Vereiste methode en beperkingen:
      • Alle onderdelen die met het schot te maken hebben waren volledig ten noordoosten van de ‘schietlijn’ op het moment dat de bal werd losgelaten richting het doel.
      ", "objectives": [{ "id": "ballshot", - "title": "Ball shot from east/north of \"Shot Lines\" toward Net", + "title": "Schot genomen vanuit positie Noord-Oost van de lijn", "type": "yesno" }, { "id": "ballscored", - "title": "Ball touching mat in Net at end of match", + "title": "Bal raakt de mat in het doel aan het eind van de wedstrijd", "type": "yesno" }], "score": [function(ballshot, ballscored) { @@ -773,15 +198,15 @@ define('services/ng-challenge',[ } }] }, { - "title": "Robotics Competition", - "description": "Todo: Kenny", + "title": "Robotwedstrijden", + "description": "De zichtbare situatie aan het einde van de wedstrijd:
      • Het blauw-geel-rode robotelement (model) is geïnstalleerd in de robotarm zoals zichtbaar op de afbeelding.
      • De lus raakt niet langer de robotarm aan.
      Vereiste methode en beperkingen:
      • Geen strategisch object raakt de robotarm aan.
      • De lus werd alleen door het gebruik van de zwarte schuif losgemaakt.
      ", "objectives": [{ "id": "roboticsinsert", - "title": "Only Robotics Insert installed", + "title": "Alleen het robotelement is geïnstalleerd in de robotarm", "type": "yesno" }, { "id": "competitionloop", - "title": "Loop no longer touching model", + "title": "Lus raakt de robotarm niet aan", "type": "yesno" }], "score": [function(roboticsinsert, competitionloop) { @@ -799,11 +224,11 @@ define('services/ng-challenge',[ } }] }, { - "title": "Using the Right Senses", - "description": "Todo: Kenny", + "title": "Gebruik de juiste zintuigen en leerstijlen", + "description": "De zichtbare situatie aan het einde van de wedstrijd:
      • De lus raakt het zintuigen model niet meer aan.
      Vereiste methode en beperkingen:
      • De lus werd alleen door het gebruik van de schuif losgemaakt.
      ", "objectives": [{ "id": "sensesloop", - "title": "Loop no longer touching model", + "title": "Lus raakt het model niet aan", "type": "yesno" }], "score": [function(sensesloop) { @@ -815,11 +240,11 @@ define('services/ng-challenge',[ } }] }, { - "title": "Remote Communication / Learning", - "description": "Todo: Kenny", + "title": "Leren/communiceren op afstand", + "description": "De zichtbare situatie aan het einde van de wedstrijd:
      • Geen.
      Vereiste methode en beperkingen:
      • De scheidsrechter heeft gezien dat de schuif door de robot westwaarts is verplaatst.
      ", "objectives": [{ "id": "pullslider", - "title": "Referee saw robot pull slider west", + "title": "Scheids zag de robot de schuif verplaatsen", "type": "yesno" }], "score": [function(pullslider) { @@ -831,15 +256,15 @@ define('services/ng-challenge',[ } }] }, { - "title": "Thinking Outside the Box", - "description": "Todo: Kenny", + "title": "Outside the Box denken", + "description": "De zichtbare situatie aan het einde van de wedstrijd:
      • Het ‘idee-model’ raakt niet langer het ‘doos-model’ aan.
      • Als het ‘idee-model’ het ‘doos-model’ niet meer aanraakt, is de afbeelding van de gloeilamp van bovenaf zichtbaar.
      Vereiste methode en beperkingen:
      • Het ‘doos-model’ is nooit in de basis geweest.
      ", "objectives": [{ "id": "bulbup", - "title": "Idea model not touching Box, Box never in Base, Bulb faces UP", + "title": "Idee-model raakt de doos niet, Doos niet in basis geweest, Lamp is naar boven gericht", "type": "yesno" }, { "id": "bulbdown", - "title": "Idea model not touching Box, Box never in Base, Bulb faces DOWN", + "title": "Idee-model raakt de doos niet, Doos niet in basis geweest, Lamp is naar beneden gericht", "type": "yesno" }], "score": [function(bulbup, bulbdown) { @@ -853,15 +278,15 @@ define('services/ng-challenge',[ return 40 } if (bulbup === 'yes' && bulbdown === 'yes') { - return new Error("Bulb cannot face UP and DOWN at the same time") + return new Error("De lamp kan niet tegelijk naar boven en beneden gericht zijn") } }] }, { - "title": "Community Learning", - "description": "Todo: Kenny", + "title": "Gemeenschappelijk leren", + "description": "De zichtbare situatie aan het einde van de wedstrijd:
      • De ‘kennis & vaardigheden lus’ raakt het gemeenschapsmodel niet meer aan.
      Vereiste methode en beperkingen:
      • Geen.
      ", "objectives": [{ "id": "communityloop", - "title": "Loop no longer touching model", + "title": "Lus raakt het model niet aan", "type": "yesno" }], "score": [function(communityloop) { @@ -873,11 +298,11 @@ define('services/ng-challenge',[ } }] }, { - "title": "Cloud Access", - "description": "Todo: Kenny", + "title": "Cloud toegang", + "description": "De zichtbare situatie aan het einde van de wedstrijd:
      • De SD-kaart staat omhoog.
      Vereiste methode en beperkingen:
      • De juiste “key” is in de Cloud geplaatst.
      ", "objectives": [{ "id": "sdcardup", - "title": "SD card is UP due to inserted \"key\"", + "title": "SD card staat omhoog omdat de juiste \"key\" is ingebracht", "type": "yesno" }], "score": [function(sdcardup) { @@ -889,53 +314,53 @@ define('services/ng-challenge',[ } }] }, { - "title": "Engagement", - "description": "Todo: Kenny", + "title": "Betrokkenheid", + "description": "De zichtbare situatie aan het einde van de wedstrijd:
      • Het gele gedeelte is naar het zuiden verplaatst.
      • Het rad is duidelijk met de klok mee gedraaid ten opzichte van de start positie. Zie het overzicht voor de score.
      Vereiste methode en beperkingen:
      • De wijzer mag alleen verplaatst worden doordat de robot aan het rad te draait.
      • De robot mag het rad maar een keer 180⁰ draaien, per keer dat de basis wordt verlaten. De scheidsrechter zal extra draaiingen ongedaan maken
      ", "objectives": [{ "id": "yellow_moved", - "title": "Yellow section moved south", + "title": "Gele gedeelte is naar het zuiden verplaatst", "type": "yesno" }, { "id": "dial_major_color", - "title": "Dial major marker color", + "title": "Aangewezen kleur", "options": [{ "value": "na", - "title": "N/A" + "title": "NVT" }, { "value": "red10", - "title": "Red 10%" + "title": "Rood 10%" }, { "value": "orange16", - "title": "Orange 16%" + "title": "Oranje 16%" }, { "value": "green22", - "title": "Green 22%" + "title": "Groen 22%" }, { "value": "blue28", - "title": "Blue 28%" + "title": "Blauw 28%" }, { "value": "red34", - "title": "Red 34%" + "title": "Rood 34%" }, { "value": "blue40", - "title": "Blue 40%" + "title": "Blauw 40%" }, { "value": "green46", - "title": "Green 46%" + "title": "Groen 46%" }, { "value": "orange52", - "title": "Orange 52%" + "title": "Oranje 52%" }, { "value": "red58", - "title": "Red 58%" + "title": "Rood 58%" }], "type": "enum" }, { "id": "ticks_past_major", - "title": "Ticks past major marker", + "title": "Klikken voorbij de kleur", "options": [{ "value": "na", - "title": "N/A" + "title": "NVT" }, { "value": "0", "title": "0" @@ -969,244 +394,244 @@ define('services/ng-challenge',[ return 0 } if (yellow_moved === 'no' && dial_major_color === 'red10' && ticks_past_major === 'na') { - return new Error("Either none or both questions should be answered with \"N/A\"") + return new Error("Er moet of twee keer \"NVT\" of twee keer een waarde worden gekozen") } if (yellow_moved === 'no' && dial_major_color === 'orange16' && ticks_past_major === 'na') { - return new Error("Either none or both questions should be answered with \"N/A\"") + return new Error("Er moet of twee keer \"NVT\" of twee keer een waarde worden gekozen") } if (yellow_moved === 'no' && dial_major_color === 'green22' && ticks_past_major === 'na') { - return new Error("Either none or both questions should be answered with \"N/A\"") + return new Error("Er moet of twee keer \"NVT\" of twee keer een waarde worden gekozen") } if (yellow_moved === 'no' && dial_major_color === 'blue28' && ticks_past_major === 'na') { - return new Error("Either none or both questions should be answered with \"N/A\"") + return new Error("Er moet of twee keer \"NVT\" of twee keer een waarde worden gekozen") } if (yellow_moved === 'no' && dial_major_color === 'red34' && ticks_past_major === 'na') { - return new Error("Either none or both questions should be answered with \"N/A\"") + return new Error("Er moet of twee keer \"NVT\" of twee keer een waarde worden gekozen") } if (yellow_moved === 'no' && dial_major_color === 'blue40' && ticks_past_major === 'na') { - return new Error("Either none or both questions should be answered with \"N/A\"") + return new Error("Er moet of twee keer \"NVT\" of twee keer een waarde worden gekozen") } if (yellow_moved === 'no' && dial_major_color === 'green46' && ticks_past_major === 'na') { - return new Error("Either none or both questions should be answered with \"N/A\"") + return new Error("Er moet of twee keer \"NVT\" of twee keer een waarde worden gekozen") } if (yellow_moved === 'no' && dial_major_color === 'orange52' && ticks_past_major === 'na') { - return new Error("Either none or both questions should be answered with \"N/A\"") + return new Error("Er moet of twee keer \"NVT\" of twee keer een waarde worden gekozen") } if (yellow_moved === 'no' && dial_major_color === 'red58' && ticks_past_major === 'na') { - return new Error("Either none or both questions should be answered with \"N/A\"") + return new Error("Er moet of twee keer \"NVT\" of twee keer een waarde worden gekozen") } if (yellow_moved === 'no' && dial_major_color === 'na' && ticks_past_major === '0') { - return new Error("Either none or both questions should be answered with \"N/A\"") + return new Error("Er moet of twee keer \"NVT\" of twee keer een waarde worden gekozen") } if (yellow_moved === 'no' && dial_major_color === 'red10' && ticks_past_major === '0') { - return new Error("Dial must remain on \"N/A\" until yellow section has moved") + return new Error("De wijzer blijft op \"NVT\" staan als het gele gedeelte niet geactiveerd is") } if (yellow_moved === 'no' && dial_major_color === 'orange16' && ticks_past_major === '0') { - return new Error("Dial must remain on \"N/A\" until yellow section has moved") + return new Error("De wijzer blijft op \"NVT\" staan als het gele gedeelte niet geactiveerd is") } if (yellow_moved === 'no' && dial_major_color === 'green22' && ticks_past_major === '0') { - return new Error("Dial must remain on \"N/A\" until yellow section has moved") + return new Error("De wijzer blijft op \"NVT\" staan als het gele gedeelte niet geactiveerd is") } if (yellow_moved === 'no' && dial_major_color === 'blue28' && ticks_past_major === '0') { - return new Error("Dial must remain on \"N/A\" until yellow section has moved") + return new Error("De wijzer blijft op \"NVT\" staan als het gele gedeelte niet geactiveerd is") } if (yellow_moved === 'no' && dial_major_color === 'red34' && ticks_past_major === '0') { - return new Error("Dial must remain on \"N/A\" until yellow section has moved") + return new Error("De wijzer blijft op \"NVT\" staan als het gele gedeelte niet geactiveerd is") } if (yellow_moved === 'no' && dial_major_color === 'blue40' && ticks_past_major === '0') { - return new Error("Dial must remain on \"N/A\" until yellow section has moved") + return new Error("De wijzer blijft op \"NVT\" staan als het gele gedeelte niet geactiveerd is") } if (yellow_moved === 'no' && dial_major_color === 'green46' && ticks_past_major === '0') { - return new Error("Dial must remain on \"N/A\" until yellow section has moved") + return new Error("De wijzer blijft op \"NVT\" staan als het gele gedeelte niet geactiveerd is") } if (yellow_moved === 'no' && dial_major_color === 'orange52' && ticks_past_major === '0') { - return new Error("Dial must remain on \"N/A\" until yellow section has moved") + return new Error("De wijzer blijft op \"NVT\" staan als het gele gedeelte niet geactiveerd is") } if (yellow_moved === 'no' && dial_major_color === 'red58' && ticks_past_major === '0') { - return new Error("Dial must remain on \"N/A\" until yellow section has moved") + return new Error("De wijzer blijft op \"NVT\" staan als het gele gedeelte niet geactiveerd is") } if (yellow_moved === 'no' && dial_major_color === 'na' && ticks_past_major === '1') { - return new Error("Either none or both questions should be answered with \"N/A\"") + return new Error("Er moet of twee keer \"NVT\" of twee keer een waarde worden gekozen") } if (yellow_moved === 'no' && dial_major_color === 'red10' && ticks_past_major === '1') { - return new Error("Dial must remain on \"N/A\" until yellow section has moved") + return new Error("De wijzer blijft op \"NVT\" staan als het gele gedeelte niet geactiveerd is") } if (yellow_moved === 'no' && dial_major_color === 'orange16' && ticks_past_major === '1') { - return new Error("Dial must remain on \"N/A\" until yellow section has moved") + return new Error("De wijzer blijft op \"NVT\" staan als het gele gedeelte niet geactiveerd is") } if (yellow_moved === 'no' && dial_major_color === 'green22' && ticks_past_major === '1') { - return new Error("Dial must remain on \"N/A\" until yellow section has moved") + return new Error("De wijzer blijft op \"NVT\" staan als het gele gedeelte niet geactiveerd is") } if (yellow_moved === 'no' && dial_major_color === 'blue28' && ticks_past_major === '1') { - return new Error("Dial must remain on \"N/A\" until yellow section has moved") + return new Error("De wijzer blijft op \"NVT\" staan als het gele gedeelte niet geactiveerd is") } if (yellow_moved === 'no' && dial_major_color === 'red34' && ticks_past_major === '1') { - return new Error("Dial must remain on \"N/A\" until yellow section has moved") + return new Error("De wijzer blijft op \"NVT\" staan als het gele gedeelte niet geactiveerd is") } if (yellow_moved === 'no' && dial_major_color === 'blue40' && ticks_past_major === '1') { - return new Error("Dial must remain on \"N/A\" until yellow section has moved") + return new Error("De wijzer blijft op \"NVT\" staan als het gele gedeelte niet geactiveerd is") } if (yellow_moved === 'no' && dial_major_color === 'green46' && ticks_past_major === '1') { - return new Error("Dial must remain on \"N/A\" until yellow section has moved") + return new Error("De wijzer blijft op \"NVT\" staan als het gele gedeelte niet geactiveerd is") } if (yellow_moved === 'no' && dial_major_color === 'orange52' && ticks_past_major === '1') { - return new Error("Dial must remain on \"N/A\" until yellow section has moved") + return new Error("De wijzer blijft op \"NVT\" staan als het gele gedeelte niet geactiveerd is") } if (yellow_moved === 'no' && dial_major_color === 'red58' && ticks_past_major === '1') { - return new Error("Dial must remain on \"N/A\" until yellow section has moved") + return new Error("De wijzer blijft op \"NVT\" staan als het gele gedeelte niet geactiveerd is") } if (yellow_moved === 'no' && dial_major_color === 'na' && ticks_past_major === '2') { - return new Error("Either none or both questions should be answered with \"N/A\"") + return new Error("Er moet of twee keer \"NVT\" of twee keer een waarde worden gekozen") } if (yellow_moved === 'no' && dial_major_color === 'red10' && ticks_past_major === '2') { - return new Error("Dial must remain on \"N/A\" until yellow section has moved") + return new Error("De wijzer blijft op \"NVT\" staan als het gele gedeelte niet geactiveerd is") } if (yellow_moved === 'no' && dial_major_color === 'orange16' && ticks_past_major === '2') { - return new Error("Dial must remain on \"N/A\" until yellow section has moved") + return new Error("De wijzer blijft op \"NVT\" staan als het gele gedeelte niet geactiveerd is") } if (yellow_moved === 'no' && dial_major_color === 'green22' && ticks_past_major === '2') { - return new Error("Dial must remain on \"N/A\" until yellow section has moved") + return new Error("De wijzer blijft op \"NVT\" staan als het gele gedeelte niet geactiveerd is") } if (yellow_moved === 'no' && dial_major_color === 'blue28' && ticks_past_major === '2') { - return new Error("Dial must remain on \"N/A\" until yellow section has moved") + return new Error("De wijzer blijft op \"NVT\" staan als het gele gedeelte niet geactiveerd is") } if (yellow_moved === 'no' && dial_major_color === 'red34' && ticks_past_major === '2') { - return new Error("Dial must remain on \"N/A\" until yellow section has moved") + return new Error("De wijzer blijft op \"NVT\" staan als het gele gedeelte niet geactiveerd is") } if (yellow_moved === 'no' && dial_major_color === 'blue40' && ticks_past_major === '2') { - return new Error("Dial must remain on \"N/A\" until yellow section has moved") + return new Error("De wijzer blijft op \"NVT\" staan als het gele gedeelte niet geactiveerd is") } if (yellow_moved === 'no' && dial_major_color === 'green46' && ticks_past_major === '2') { - return new Error("Dial must remain on \"N/A\" until yellow section has moved") + return new Error("De wijzer blijft op \"NVT\" staan als het gele gedeelte niet geactiveerd is") } if (yellow_moved === 'no' && dial_major_color === 'orange52' && ticks_past_major === '2') { - return new Error("Dial must remain on \"N/A\" until yellow section has moved") + return new Error("De wijzer blijft op \"NVT\" staan als het gele gedeelte niet geactiveerd is") } if (yellow_moved === 'no' && dial_major_color === 'red58' && ticks_past_major === '2') { - return new Error("Dial must remain on \"N/A\" until yellow section has moved") + return new Error("De wijzer blijft op \"NVT\" staan als het gele gedeelte niet geactiveerd is") } if (yellow_moved === 'no' && dial_major_color === 'na' && ticks_past_major === '3') { - return new Error("Either none or both questions should be answered with \"N/A\"") + return new Error("Er moet of twee keer \"NVT\" of twee keer een waarde worden gekozen") } if (yellow_moved === 'no' && dial_major_color === 'red10' && ticks_past_major === '3') { - return new Error("Dial must remain on \"N/A\" until yellow section has moved") + return new Error("De wijzer blijft op \"NVT\" staan als het gele gedeelte niet geactiveerd is") } if (yellow_moved === 'no' && dial_major_color === 'orange16' && ticks_past_major === '3') { - return new Error("Dial must remain on \"N/A\" until yellow section has moved") + return new Error("De wijzer blijft op \"NVT\" staan als het gele gedeelte niet geactiveerd is") } if (yellow_moved === 'no' && dial_major_color === 'green22' && ticks_past_major === '3') { - return new Error("Dial must remain on \"N/A\" until yellow section has moved") + return new Error("De wijzer blijft op \"NVT\" staan als het gele gedeelte niet geactiveerd is") } if (yellow_moved === 'no' && dial_major_color === 'blue28' && ticks_past_major === '3') { - return new Error("Dial must remain on \"N/A\" until yellow section has moved") + return new Error("De wijzer blijft op \"NVT\" staan als het gele gedeelte niet geactiveerd is") } if (yellow_moved === 'no' && dial_major_color === 'red34' && ticks_past_major === '3') { - return new Error("Dial must remain on \"N/A\" until yellow section has moved") + return new Error("De wijzer blijft op \"NVT\" staan als het gele gedeelte niet geactiveerd is") } if (yellow_moved === 'no' && dial_major_color === 'blue40' && ticks_past_major === '3') { - return new Error("Dial must remain on \"N/A\" until yellow section has moved") + return new Error("De wijzer blijft op \"NVT\" staan als het gele gedeelte niet geactiveerd is") } if (yellow_moved === 'no' && dial_major_color === 'green46' && ticks_past_major === '3') { - return new Error("Dial must remain on \"N/A\" until yellow section has moved") + return new Error("De wijzer blijft op \"NVT\" staan als het gele gedeelte niet geactiveerd is") } if (yellow_moved === 'no' && dial_major_color === 'orange52' && ticks_past_major === '3') { - return new Error("Dial must remain on \"N/A\" until yellow section has moved") + return new Error("De wijzer blijft op \"NVT\" staan als het gele gedeelte niet geactiveerd is") } if (yellow_moved === 'no' && dial_major_color === 'red58' && ticks_past_major === '3') { - return new Error("Dial must remain on \"N/A\" until yellow section has moved") + return new Error("De wijzer blijft op \"NVT\" staan als het gele gedeelte niet geactiveerd is") } if (yellow_moved === 'no' && dial_major_color === 'na' && ticks_past_major === '4') { - return new Error("Either none or both questions should be answered with \"N/A\"") + return new Error("Er moet of twee keer \"NVT\" of twee keer een waarde worden gekozen") } if (yellow_moved === 'no' && dial_major_color === 'red10' && ticks_past_major === '4') { - return new Error("Dial must remain on \"N/A\" until yellow section has moved") + return new Error("De wijzer blijft op \"NVT\" staan als het gele gedeelte niet geactiveerd is") } if (yellow_moved === 'no' && dial_major_color === 'orange16' && ticks_past_major === '4') { - return new Error("Dial must remain on \"N/A\" until yellow section has moved") + return new Error("De wijzer blijft op \"NVT\" staan als het gele gedeelte niet geactiveerd is") } if (yellow_moved === 'no' && dial_major_color === 'green22' && ticks_past_major === '4') { - return new Error("Dial must remain on \"N/A\" until yellow section has moved") + return new Error("De wijzer blijft op \"NVT\" staan als het gele gedeelte niet geactiveerd is") } if (yellow_moved === 'no' && dial_major_color === 'blue28' && ticks_past_major === '4') { - return new Error("Dial must remain on \"N/A\" until yellow section has moved") + return new Error("De wijzer blijft op \"NVT\" staan als het gele gedeelte niet geactiveerd is") } if (yellow_moved === 'no' && dial_major_color === 'red34' && ticks_past_major === '4') { - return new Error("Dial must remain on \"N/A\" until yellow section has moved") + return new Error("De wijzer blijft op \"NVT\" staan als het gele gedeelte niet geactiveerd is") } if (yellow_moved === 'no' && dial_major_color === 'blue40' && ticks_past_major === '4') { - return new Error("Dial must remain on \"N/A\" until yellow section has moved") + return new Error("De wijzer blijft op \"NVT\" staan als het gele gedeelte niet geactiveerd is") } if (yellow_moved === 'no' && dial_major_color === 'green46' && ticks_past_major === '4') { - return new Error("Dial must remain on \"N/A\" until yellow section has moved") + return new Error("De wijzer blijft op \"NVT\" staan als het gele gedeelte niet geactiveerd is") } if (yellow_moved === 'no' && dial_major_color === 'orange52' && ticks_past_major === '4') { - return new Error("Dial must remain on \"N/A\" until yellow section has moved") + return new Error("De wijzer blijft op \"NVT\" staan als het gele gedeelte niet geactiveerd is") } if (yellow_moved === 'no' && dial_major_color === 'red58' && ticks_past_major === '4') { - return new Error("Dial must remain on \"N/A\" until yellow section has moved") + return new Error("De wijzer blijft op \"NVT\" staan als het gele gedeelte niet geactiveerd is") } if (yellow_moved === 'no' && dial_major_color === 'na' && ticks_past_major === '5') { - return new Error("Either none or both questions should be answered with \"N/A\"") + return new Error("Er moet of twee keer \"NVT\" of twee keer een waarde worden gekozen") } if (yellow_moved === 'no' && dial_major_color === 'red10' && ticks_past_major === '5') { - return new Error("Dial must remain on \"N/A\" until yellow section has moved") + return new Error("De wijzer blijft op \"NVT\" staan als het gele gedeelte niet geactiveerd is") } if (yellow_moved === 'no' && dial_major_color === 'orange16' && ticks_past_major === '5') { - return new Error("Dial must remain on \"N/A\" until yellow section has moved") + return new Error("De wijzer blijft op \"NVT\" staan als het gele gedeelte niet geactiveerd is") } if (yellow_moved === 'no' && dial_major_color === 'green22' && ticks_past_major === '5') { - return new Error("Dial must remain on \"N/A\" until yellow section has moved") + return new Error("De wijzer blijft op \"NVT\" staan als het gele gedeelte niet geactiveerd is") } if (yellow_moved === 'no' && dial_major_color === 'blue28' && ticks_past_major === '5') { - return new Error("Dial must remain on \"N/A\" until yellow section has moved") + return new Error("De wijzer blijft op \"NVT\" staan als het gele gedeelte niet geactiveerd is") } if (yellow_moved === 'no' && dial_major_color === 'red34' && ticks_past_major === '5') { - return new Error("Dial must remain on \"N/A\" until yellow section has moved") + return new Error("De wijzer blijft op \"NVT\" staan als het gele gedeelte niet geactiveerd is") } if (yellow_moved === 'no' && dial_major_color === 'blue40' && ticks_past_major === '5') { - return new Error("Dial must remain on \"N/A\" until yellow section has moved") + return new Error("De wijzer blijft op \"NVT\" staan als het gele gedeelte niet geactiveerd is") } if (yellow_moved === 'no' && dial_major_color === 'green46' && ticks_past_major === '5') { - return new Error("Dial must remain on \"N/A\" until yellow section has moved") + return new Error("De wijzer blijft op \"NVT\" staan als het gele gedeelte niet geactiveerd is") } if (yellow_moved === 'no' && dial_major_color === 'orange52' && ticks_past_major === '5') { - return new Error("Dial must remain on \"N/A\" until yellow section has moved") + return new Error("De wijzer blijft op \"NVT\" staan als het gele gedeelte niet geactiveerd is") } if (yellow_moved === 'no' && dial_major_color === 'red58' && ticks_past_major === '5') { - return new Error("Dial must remain on \"N/A\" until yellow section has moved") + return new Error("De wijzer blijft op \"NVT\" staan als het gele gedeelte niet geactiveerd is") } if (yellow_moved === 'yes' && dial_major_color === 'na' && ticks_past_major === 'na') { return 0 } if (yellow_moved === 'yes' && dial_major_color === 'red10' && ticks_past_major === 'na') { - return new Error("Either none or both questions should be answered with \"N/A\"") + return new Error("Er moet of twee keer \"NVT\" of twee keer een waarde worden gekozen") } if (yellow_moved === 'yes' && dial_major_color === 'orange16' && ticks_past_major === 'na') { - return new Error("Either none or both questions should be answered with \"N/A\"") + return new Error("Er moet of twee keer \"NVT\" of twee keer een waarde worden gekozen") } if (yellow_moved === 'yes' && dial_major_color === 'green22' && ticks_past_major === 'na') { - return new Error("Either none or both questions should be answered with \"N/A\"") + return new Error("Er moet of twee keer \"NVT\" of twee keer een waarde worden gekozen") } if (yellow_moved === 'yes' && dial_major_color === 'blue28' && ticks_past_major === 'na') { - return new Error("Either none or both questions should be answered with \"N/A\"") + return new Error("Er moet of twee keer \"NVT\" of twee keer een waarde worden gekozen") } if (yellow_moved === 'yes' && dial_major_color === 'red34' && ticks_past_major === 'na') { - return new Error("Either none or both questions should be answered with \"N/A\"") + return new Error("Er moet of twee keer \"NVT\" of twee keer een waarde worden gekozen") } if (yellow_moved === 'yes' && dial_major_color === 'blue40' && ticks_past_major === 'na') { - return new Error("Either none or both questions should be answered with \"N/A\"") + return new Error("Er moet of twee keer \"NVT\" of twee keer een waarde worden gekozen") } if (yellow_moved === 'yes' && dial_major_color === 'green46' && ticks_past_major === 'na') { - return new Error("Either none or both questions should be answered with \"N/A\"") + return new Error("Er moet of twee keer \"NVT\" of twee keer een waarde worden gekozen") } if (yellow_moved === 'yes' && dial_major_color === 'orange52' && ticks_past_major === 'na') { - return new Error("Either none or both questions should be answered with \"N/A\"") + return new Error("Er moet of twee keer \"NVT\" of twee keer een waarde worden gekozen") } if (yellow_moved === 'yes' && dial_major_color === 'red58' && ticks_past_major === 'na') { - return new Error("Either none or both questions should be answered with \"N/A\"") + return new Error("Er moet of twee keer \"NVT\" of twee keer een waarde worden gekozen") } if (yellow_moved === 'yes' && dial_major_color === 'na' && ticks_past_major === '0') { - return new Error("Either none or both questions should be answered with \"N/A\"") + return new Error("Er moet of twee keer \"NVT\" of twee keer een waarde worden gekozen") } if (yellow_moved === 'yes' && dial_major_color === 'red10' && ticks_past_major === '0') { return 0.1 @@ -1236,7 +661,7 @@ define('services/ng-challenge',[ return 0.58 } if (yellow_moved === 'yes' && dial_major_color === 'na' && ticks_past_major === '1') { - return new Error("Either none or both questions should be answered with \"N/A\"") + return new Error("Er moet of twee keer \"NVT\" of twee keer een waarde worden gekozen") } if (yellow_moved === 'yes' && dial_major_color === 'red10' && ticks_past_major === '1') { return 0.11 @@ -1266,7 +691,7 @@ define('services/ng-challenge',[ return 0.58 } if (yellow_moved === 'yes' && dial_major_color === 'na' && ticks_past_major === '2') { - return new Error("Either none or both questions should be answered with \"N/A\"") + return new Error("Er moet of twee keer \"NVT\" of twee keer een waarde worden gekozen") } if (yellow_moved === 'yes' && dial_major_color === 'red10' && ticks_past_major === '2') { return 0.12 @@ -1293,10 +718,10 @@ define('services/ng-challenge',[ return 0.54 } if (yellow_moved === 'yes' && dial_major_color === 'red58' && ticks_past_major === '2') { - return new Error("Dial cannot turn that far") + return new Error("De wijzer kan niet zover draaien") } if (yellow_moved === 'yes' && dial_major_color === 'na' && ticks_past_major === '3') { - return new Error("Either none or both questions should be answered with \"N/A\"") + return new Error("Er moet of twee keer \"NVT\" of twee keer een waarde worden gekozen") } if (yellow_moved === 'yes' && dial_major_color === 'red10' && ticks_past_major === '3') { return 0.13 @@ -1323,10 +748,10 @@ define('services/ng-challenge',[ return 0.55 } if (yellow_moved === 'yes' && dial_major_color === 'red58' && ticks_past_major === '3') { - return new Error("Dial cannot turn that far") + return new Error("De wijzer kan niet zover draaien") } if (yellow_moved === 'yes' && dial_major_color === 'na' && ticks_past_major === '4') { - return new Error("Either none or both questions should be answered with \"N/A\"") + return new Error("Er moet of twee keer \"NVT\" of twee keer een waarde worden gekozen") } if (yellow_moved === 'yes' && dial_major_color === 'red10' && ticks_past_major === '4') { return 0.14 @@ -1353,10 +778,10 @@ define('services/ng-challenge',[ return 0.56 } if (yellow_moved === 'yes' && dial_major_color === 'red58' && ticks_past_major === '4') { - return new Error("Dial cannot turn that far") + return new Error("De wijzer kan niet zover draaien") } if (yellow_moved === 'yes' && dial_major_color === 'na' && ticks_past_major === '5') { - return new Error("Either none or both questions should be answered with \"N/A\"") + return new Error("Er moet of twee keer \"NVT\" of twee keer een waarde worden gekozen") } if (yellow_moved === 'yes' && dial_major_color === 'red10' && ticks_past_major === '5') { return 0.15 @@ -1383,15 +808,15 @@ define('services/ng-challenge',[ return 0.57 } if (yellow_moved === 'yes' && dial_major_color === 'red58' && ticks_past_major === '5') { - return new Error("Dial cannot turn that far") + return new Error("De wijzer kan niet zover draaien") } }] }, { - "title": "Adapting to changing conditions", - "description": "Todo: Kenny", + "title": "Flexibiliteit", + "description": "De zichtbare situatie aan het einde van de wedstrijd:
      • Het model is 90⁰ gedraaid tegen de richting van de klok in ten opzichte van de beginpositie.
      Vereiste methode en beperkingen:
      • Geen.
      ", "objectives": [{ "id": "model_rotated", - "title": "Model rotated 90-ish degrees CCW", + "title": "Model is 90 graden tegen de klok in gedraaid", "type": "yesno" }], "score": [function(model_rotated) { @@ -1403,14 +828,40 @@ define('services/ng-challenge',[ } }] }, { - "title": "Penalties", + "title": "Strafpunten", "description": "training-desc", "objectives": [{ "id": "penalties_objective", - "title": "Robot, Sprawl, Junk penalties", - "type": "number", - "min": "0", - "max": "8" + "title": "Robot, rommel of uitvouwstrafpunten", + "options": [{ + "value": "0", + "title": "0" + }, { + "value": "1", + "title": "1" + }, { + "value": "2", + "title": "2" + }, { + "value": "3", + "title": "3" + }, { + "value": "4", + "title": "4" + }, { + "value": "5", + "title": "5" + }, { + "value": "6", + "title": "6" + }, { + "value": "7", + "title": "7" + }, { + "value": "8", + "title": "8" + }], + "type": "enum" }], "score": [function(penalties_objective) { if (penalties_objective === '0') { @@ -1446,6 +897,7 @@ define('services/ng-challenge',[ + function indexObjectives(missions) { objs = {}; angular.forEach(missions,function(mission) { From 35a8fa98986cb8e34b660c921a535f3e9f7d26cb Mon Sep 17 00:00:00 2001 From: rikkertkoppes Date: Mon, 24 Nov 2014 12:37:58 +0100 Subject: [PATCH 14/29] Added comments --- localserver.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/localserver.js b/localserver.js index 269a7d90..875eedf9 100644 --- a/localserver.js +++ b/localserver.js @@ -39,7 +39,7 @@ app.use(function(req, res, next) { }); }); - +//reading the "file system" app.get(/^\/fs\/(.*)$/, function(req, res) { var path = __dirname + '/data/' + req.params[0]; fs.stat(path, function(err, stat) { @@ -106,6 +106,7 @@ function writeFile(path, contents, cb) { }); } +// writing the "file system" app.post(/^\/fs\/(.*)$/, function(req, res) { var path = __dirname + '/data/' + req.params[0]; writeFile(path, req.body, function(err) { @@ -117,6 +118,7 @@ app.post(/^\/fs\/(.*)$/, function(req, res) { }); }); +// deleting in the "file system" app.delete(/^\/fs\/(.*)$/, function(req, res) { var path = __dirname + '/data/' + req.params[0]; fs.unlink(path, function(err) { From d509f1d7d85470b5a78d33dd2c290a78c4f9cf6c Mon Sep 17 00:00:00 2001 From: rikkertkoppes Date: Mon, 24 Nov 2014 12:46:53 +0100 Subject: [PATCH 15/29] First setup for separate scoring entry --- src/js/main.js | 2 ++ src/scoring.html | 23 +++++++++++++++++++++++ src/views/main.html | 1 - src/views/mainScoring.html | 16 ++++++++++++++++ 4 files changed, 41 insertions(+), 1 deletion(-) create mode 100644 src/scoring.html create mode 100644 src/views/mainScoring.html diff --git a/src/js/main.js b/src/js/main.js index d84cc4ff..e9f83aae 100644 --- a/src/js/main.js +++ b/src/js/main.js @@ -36,7 +36,9 @@ define([ function($scope, $scores) { log('init main ctrl'); $scope.mainView = 'views/main.html'; + $scope.scoringView = 'views/mainScoring.html'; $scope.pages = ['teams','scoresheet','scores','ranking','settings']; + $scope.scoringPages = ['scoresheet','settings']; $scope.currentPage = $scope.pages[1]; $scope.validationErrors = $scores.validationErrors; diff --git a/src/scoring.html b/src/scoring.html new file mode 100644 index 00000000..057e3069 --- /dev/null +++ b/src/scoring.html @@ -0,0 +1,23 @@ + + + + + + + + + FLL Scoring + + + + + +
      + + diff --git a/src/views/main.html b/src/views/main.html index 93787dd9..dbdad435 100644 --- a/src/views/main.html +++ b/src/views/main.html @@ -14,7 +14,6 @@
      diff --git a/src/views/mainScoring.html b/src/views/mainScoring.html new file mode 100644 index 00000000..22227dff --- /dev/null +++ b/src/views/mainScoring.html @@ -0,0 +1,16 @@ +
      + +
      +
      +
      +
      \ No newline at end of file From 804e21a47350081d5892b43a983dee432da1b412 Mon Sep 17 00:00:00 2001 From: rikkertkoppes Date: Tue, 25 Nov 2014 12:46:08 +0100 Subject: [PATCH 16/29] Added appcache, see #96 --- src/fllscoring.appcache | 74 +++++++++++++++++++++++++++++++++++++++++ src/index.html | 2 +- 2 files changed, 75 insertions(+), 1 deletion(-) create mode 100644 src/fllscoring.appcache diff --git a/src/fllscoring.appcache b/src/fllscoring.appcache new file mode 100644 index 00000000..a1721aa1 --- /dev/null +++ b/src/fllscoring.appcache @@ -0,0 +1,74 @@ +CACHE MANIFEST +#2014-11-25 1230 + +CACHE: +index.html +scoring.html +views/main.html +views/mainScoring.html +views/ranking.html +views/scores.html +views/scoresheet.html +views/settings.html +views/teams.html + +css/elements.css +css/main.css +css/scoresheet.css +css/spinner.css +css/teams.css + +fonts/lato-lig-webfont.eot +fonts/lato-lig-webfont.svg +fonts/lato-lig-webfont.ttf +fonts/lato-lig-webfont.woff +fonts/lato-reg-webfont.eot +fonts/lato-reg-webfont.svg +fonts/lato-reg-webfont.ttf +fonts/lato-reg-webfont.woff +fonts/lato.css + +js/config.js +js/main.js +js/directives/ng-directives.js +js/directives/really.js +js/directives/sigpad.js +js/directives/size.js +js/directives/spinner.js +js/filters/index.js +js/filters/ng-filters.js +js/services/fs-nw.js +js/services/fs-pg.js +js/services/fs-xhr.js +js/services/log.js +js/services/ng-challenge.js +js/services/ng-connect.js +js/services/ng-fs.js +js/services/ng-scores.js +js/services/ng-services.js +js/services/ng-settings.js +js/services/ng-stages.js +js/services/ng-teams.js +js/tests/fsTest.js +js/tests/indexedDBTest.js +js/views/ranking.js +js/views/scores.js +js/views/scoresheet.js +js/views/settings.js +js/views/teams.js + +components/angular/angular.min.js +components/angular-bootstrap/ui-bootstrap-tpls.js +components/angular-sanitize/angular-sanitize.min.js +components/bootstrap-css/css/bootstrap.min.css +#components/bootstrap-css/fonts/glyphicons-halflings-regular.woff +#components/bootstrap-css/fonts/glyphicons-halflings-regular.ttf +components/fastclick/lib/fastclick.js +components/font-awesome/css/font-awesome.min.css +components/jquery/jquery.min.js +components/q/q.js +components/requirejs/require.js +components/signature-pad/jquery.signaturepad.min.js + +NETWORK: +* \ No newline at end of file diff --git a/src/index.html b/src/index.html index 4c1c6626..73d01a01 100644 --- a/src/index.html +++ b/src/index.html @@ -1,5 +1,5 @@ - + From 73060448263f4edc8843541b5c875976de6baa85 Mon Sep 17 00:00:00 2001 From: rikkertkoppes Date: Tue, 25 Nov 2014 13:15:53 +0100 Subject: [PATCH 17/29] updated dependency: coverage to devdependencies --- npm-shrinkwrap.json | 421 ++++++-------------------------------------- package.json | 2 +- 2 files changed, 56 insertions(+), 367 deletions(-) diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index f43312c1..3a156b20 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -5,78 +5,84 @@ "express": { "version": "3.4.8", "from": "express@3.4.8", + "resolved": "https://registry.npmjs.org/express/-/express-3.4.8.tgz", "dependencies": { "connect": { "version": "2.12.0", - "from": "connect@2.12.0", + "from": "https://registry.npmjs.org/connect/-/connect-2.12.0.tgz", "resolved": "https://registry.npmjs.org/connect/-/connect-2.12.0.tgz", "dependencies": { "batch": { "version": "0.5.0", - "from": "batch@0.5.0", + "from": "https://registry.npmjs.org/batch/-/batch-0.5.0.tgz", "resolved": "https://registry.npmjs.org/batch/-/batch-0.5.0.tgz" }, "qs": { "version": "0.6.6", - "from": "qs@0.6.6", + "from": "https://registry.npmjs.org/qs/-/qs-0.6.6.tgz", "resolved": "https://registry.npmjs.org/qs/-/qs-0.6.6.tgz" }, "bytes": { "version": "0.2.1", - "from": "bytes@0.2.1", + "from": "https://registry.npmjs.org/bytes/-/bytes-0.2.1.tgz", "resolved": "https://registry.npmjs.org/bytes/-/bytes-0.2.1.tgz" }, "pause": { "version": "0.0.1", - "from": "pause@0.0.1", + "from": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz", "resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz" }, "uid2": { "version": "0.0.3", - "from": "uid2@0.0.3", + "from": "https://registry.npmjs.org/uid2/-/uid2-0.0.3.tgz", "resolved": "https://registry.npmjs.org/uid2/-/uid2-0.0.3.tgz" }, "raw-body": { "version": "1.1.2", - "from": "raw-body@1.1.2", + "from": "https://registry.npmjs.org/raw-body/-/raw-body-1.1.2.tgz", "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-1.1.2.tgz" }, "negotiator": { "version": "0.3.0", - "from": "negotiator@0.3.0", + "from": "https://registry.npmjs.org/negotiator/-/negotiator-0.3.0.tgz", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.3.0.tgz" }, "multiparty": { "version": "2.2.0", - "from": "multiparty@2.2.0", + "from": "https://registry.npmjs.org/multiparty/-/multiparty-2.2.0.tgz", "resolved": "https://registry.npmjs.org/multiparty/-/multiparty-2.2.0.tgz", "dependencies": { "readable-stream": { "version": "1.1.13-1", - "from": "readable-stream@~1.1.9", + "from": "readable-stream@1.1.13-1", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.13-1.tgz", "dependencies": { "core-util-is": { "version": "1.0.1", - "from": "core-util-is@~1.0.0" + "from": "core-util-is@1.0.1", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.1.tgz" }, "isarray": { "version": "0.0.1", - "from": "isarray@0.0.1", + "from": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz" }, "string_decoder": { "version": "0.10.25-1", - "from": "string_decoder@~0.10.x" + "from": "string_decoder@0.10.25-1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.25-1.tgz" }, "inherits": { "version": "2.0.1", - "from": "inherits@~2.0.1" + "from": "inherits@2.0.1", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz" } } }, "stream-counter": { "version": "0.2.0", - "from": "stream-counter@~0.2.0" + "from": "stream-counter@0.2.0", + "resolved": "https://registry.npmjs.org/stream-counter/-/stream-counter-0.2.0.tgz" } } } @@ -84,93 +90,100 @@ }, "commander": { "version": "1.3.2", - "from": "commander@1.3.2", + "from": "https://registry.npmjs.org/commander/-/commander-1.3.2.tgz", "resolved": "https://registry.npmjs.org/commander/-/commander-1.3.2.tgz", "dependencies": { "keypress": { "version": "0.1.0", - "from": "keypress@0.1.x" + "from": "keypress@0.1.0", + "resolved": "https://registry.npmjs.org/keypress/-/keypress-0.1.0.tgz" } } }, "range-parser": { "version": "0.0.4", - "from": "range-parser@0.0.4", + "from": "https://registry.npmjs.org/range-parser/-/range-parser-0.0.4.tgz", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-0.0.4.tgz" }, "cookie": { "version": "0.1.0", - "from": "cookie@0.1.0", + "from": "https://registry.npmjs.org/cookie/-/cookie-0.1.0.tgz", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.1.0.tgz" }, "buffer-crc32": { "version": "0.2.1", - "from": "buffer-crc32@0.2.1", + "from": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.1.tgz", "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.1.tgz" }, "fresh": { "version": "0.2.0", - "from": "fresh@0.2.0", + "from": "https://registry.npmjs.org/fresh/-/fresh-0.2.0.tgz", "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.2.0.tgz" }, "methods": { "version": "0.1.0", - "from": "methods@0.1.0", + "from": "https://registry.npmjs.org/methods/-/methods-0.1.0.tgz", "resolved": "https://registry.npmjs.org/methods/-/methods-0.1.0.tgz" }, "send": { "version": "0.1.4", - "from": "send@0.1.4", + "from": "https://registry.npmjs.org/send/-/send-0.1.4.tgz", "resolved": "https://registry.npmjs.org/send/-/send-0.1.4.tgz", "dependencies": { "mime": { "version": "1.2.11", - "from": "mime@~1.2.9" + "from": "mime@1.2.11", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.2.11.tgz" } } }, "cookie-signature": { "version": "1.0.1", - "from": "cookie-signature@1.0.1", + "from": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.1.tgz", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.1.tgz" }, "merge-descriptors": { "version": "0.0.1", - "from": "merge-descriptors@0.0.1", + "from": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-0.0.1.tgz", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-0.0.1.tgz" }, "debug": { "version": "0.8.1", - "from": "debug@>= 0.7.3 < 1" + "from": "debug@0.8.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-0.8.1.tgz" } } }, "js-beautify": { "version": "1.5.4", - "from": "js-beautify@^1.5.4", + "from": "https://registry.npmjs.org/js-beautify/-/js-beautify-1.5.4.tgz", "resolved": "https://registry.npmjs.org/js-beautify/-/js-beautify-1.5.4.tgz", "dependencies": { "config-chain": { "version": "1.1.8", - "from": "config-chain@~1.1.5", + "from": "config-chain@~1.1.8", + "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.8.tgz", "dependencies": { "proto-list": { "version": "1.2.3", - "from": "proto-list@~1.2.1" + "from": "proto-list@1.2.3", + "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.3.tgz" }, "ini": { "version": "1.3.0", - "from": "ini@1" + "from": "ini@1.3.0", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.0.tgz" } } }, "mkdirp": { "version": "0.5.0", - "from": "mkdirp@~0.5.0", + "from": "mkdirp@0.5.0", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.0.tgz", "dependencies": { "minimist": { "version": "0.0.8", - "from": "minimist@0.0.8", + "from": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz" } } @@ -178,337 +191,12 @@ "nopt": { "version": "3.0.1", "from": "nopt@~3.0.1", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.1.tgz", "dependencies": { "abbrev": { "version": "1.0.5", - "from": "abbrev@1" - } - } - } - } - }, - "karma-coverage": { - "version": "0.2.4", - "from": "karma-coverage@", - "resolved": "https://registry.npmjs.org/karma-coverage/-/karma-coverage-0.2.4.tgz", - "dependencies": { - "istanbul": { - "version": "0.2.16", - "from": "istanbul@~0.2.10", - "resolved": "https://registry.npmjs.org/istanbul/-/istanbul-0.2.16.tgz", - "dependencies": { - "esprima": { - "version": "1.2.2", - "from": "esprima@1.2.x", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-1.2.2.tgz" - }, - "escodegen": { - "version": "1.3.3", - "from": "escodegen@1.3.x", - "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.3.3.tgz", - "dependencies": { - "esutils": { - "version": "1.0.0", - "from": "esutils@~1.0.0", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-1.0.0.tgz" - }, - "estraverse": { - "version": "1.5.1", - "from": "estraverse@~1.5.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-1.5.1.tgz" - }, - "esprima": { - "version": "1.1.1", - "from": "esprima@~1.1.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-1.1.1.tgz" - }, - "source-map": { - "version": "0.1.37", - "from": "source-map@~0.1.30", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.1.37.tgz", - "dependencies": { - "amdefine": { - "version": "0.1.0", - "from": "amdefine@>=0.0.4", - "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-0.1.0.tgz" - } - } - } - } - }, - "handlebars": { - "version": "1.3.0", - "from": "handlebars@1.3.x", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-1.3.0.tgz", - "dependencies": { - "optimist": { - "version": "0.3.7", - "from": "optimist@~0.3" - }, - "uglify-js": { - "version": "2.3.6", - "from": "uglify-js@~2.3", - "dependencies": { - "async": { - "version": "0.2.10", - "from": "async@~0.2.6", - "resolved": "https://registry.npmjs.org/async/-/async-0.2.10.tgz" - }, - "source-map": { - "version": "0.1.37", - "from": "source-map@~0.1.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.1.37.tgz", - "dependencies": { - "amdefine": { - "version": "0.1.0", - "from": "amdefine@>=0.0.4", - "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-0.1.0.tgz" - } - } - } - } - } - } - }, - "mkdirp": { - "version": "0.5.0", - "from": "mkdirp@0.5.x", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.0.tgz", - "dependencies": { - "minimist": { - "version": "0.0.8", - "from": "minimist@0.0.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz" - } - } - }, - "nopt": { - "version": "3.0.1", - "from": "nopt@3.x", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.1.tgz" - }, - "fileset": { - "version": "0.1.5", - "from": "fileset@0.1.x", - "dependencies": { - "glob": { - "version": "3.2.11", - "from": "glob@3.x", - "resolved": "https://registry.npmjs.org/glob/-/glob-3.2.11.tgz", - "dependencies": { - "inherits": { - "version": "2.0.1", - "from": "inherits@2" - } - } - } - } - }, - "which": { - "version": "1.0.5", - "from": "which@1.0.x" - }, - "async": { - "version": "0.9.0", - "from": "async@0.9.x", - "resolved": "https://registry.npmjs.org/async/-/async-0.9.0.tgz" - }, - "abbrev": { - "version": "1.0.5", - "from": "abbrev@1.0.x", + "from": "abbrev@1.0.5", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.0.5.tgz" - }, - "wordwrap": { - "version": "0.0.2", - "from": "wordwrap@0.0.x" - }, - "resolve": { - "version": "0.7.1", - "from": "resolve@0.7.x", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-0.7.1.tgz" - }, - "js-yaml": { - "version": "3.1.0", - "from": "js-yaml@3.x", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.1.0.tgz", - "dependencies": { - "argparse": { - "version": "0.1.15", - "from": "argparse@~ 0.1.11", - "dependencies": { - "underscore": { - "version": "1.4.4", - "from": "underscore@~1.4.3" - }, - "underscore.string": { - "version": "2.3.3", - "from": "underscore.string@~2.3.1" - } - } - }, - "esprima": { - "version": "1.0.4", - "from": "esprima@~ 1.0.2" - } - } - } - } - }, - "ibrik": { - "version": "1.1.1", - "from": "ibrik@~1.1.1", - "resolved": "https://registry.npmjs.org/ibrik/-/ibrik-1.1.1.tgz", - "dependencies": { - "lodash": { - "version": "2.4.1", - "from": "lodash@~2.4.1", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-2.4.1.tgz" - }, - "coffee-script-redux": { - "version": "2.0.0-beta8", - "from": "coffee-script-redux@=2.0.0-beta8", - "resolved": "https://registry.npmjs.org/coffee-script-redux/-/coffee-script-redux-2.0.0-beta8.tgz", - "dependencies": { - "StringScanner": { - "version": "0.0.3", - "from": "StringScanner@~0.0.3", - "resolved": "https://registry.npmjs.org/StringScanner/-/StringScanner-0.0.3.tgz" - }, - "nopt": { - "version": "2.1.2", - "from": "nopt@~2.1.2", - "dependencies": { - "abbrev": { - "version": "1.0.5", - "from": "abbrev@1", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.0.5.tgz" - } - } - }, - "esmangle": { - "version": "0.0.17", - "from": "esmangle@~0.0.8", - "resolved": "https://registry.npmjs.org/esmangle/-/esmangle-0.0.17.tgz", - "dependencies": { - "esprima": { - "version": "1.0.4", - "from": "esprima@~ 1.0.2" - }, - "escope": { - "version": "1.0.1", - "from": "escope@~ 1.0.0", - "resolved": "https://registry.npmjs.org/escope/-/escope-1.0.1.tgz" - }, - "estraverse": { - "version": "1.3.2", - "from": "estraverse@~ 1.3.2", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-1.3.2.tgz" - }, - "esshorten": { - "version": "0.0.2", - "from": "esshorten@~ 0.0.2", - "resolved": "https://registry.npmjs.org/esshorten/-/esshorten-0.0.2.tgz", - "dependencies": { - "estraverse": { - "version": "1.2.0", - "from": "estraverse@~ 1.2.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-1.2.0.tgz" - } - } - } - } - }, - "source-map": { - "version": "0.1.11", - "from": "source-map@0.1.11", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.1.11.tgz", - "dependencies": { - "amdefine": { - "version": "0.1.0", - "from": "amdefine@>=0.0.4", - "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-0.1.0.tgz" - } - } - }, - "escodegen": { - "version": "0.0.28", - "from": "escodegen@~0.0.24", - "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-0.0.28.tgz", - "dependencies": { - "esprima": { - "version": "1.0.4", - "from": "esprima@~1.0.2" - }, - "estraverse": { - "version": "1.3.2", - "from": "estraverse@~1.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-1.3.2.tgz" - } - } - } - } - }, - "estraverse": { - "version": "1.5.1", - "from": "estraverse@~1.5.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-1.5.1.tgz" - }, - "escodegen": { - "version": "1.1.0", - "from": "escodegen@~1.1.0", - "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.1.0.tgz", - "dependencies": { - "esprima": { - "version": "1.0.4", - "from": "esprima@~1.0.4" - }, - "esutils": { - "version": "1.0.0", - "from": "esutils@~1.0.0", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-1.0.0.tgz" - } - } - }, - "which": { - "version": "1.0.5", - "from": "which@~1.0.5" - }, - "optimist": { - "version": "0.6.1", - "from": "optimist@~0.6.1", - "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz", - "dependencies": { - "wordwrap": { - "version": "0.0.2", - "from": "wordwrap@~0.0.2" - }, - "minimist": { - "version": "0.0.10", - "from": "minimist@~0.0.1", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz" - } - } - } - } - }, - "dateformat": { - "version": "1.0.8-1.2.3", - "from": "dateformat@~1.0.6", - "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-1.0.8-1.2.3.tgz" - }, - "minimatch": { - "version": "0.3.0", - "from": "minimatch@~0.3.0", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.3.0.tgz", - "dependencies": { - "lru-cache": { - "version": "2.5.0", - "from": "lru-cache@2", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.5.0.tgz" - }, - "sigmund": { - "version": "1.0.0", - "from": "sigmund@~1.0.0" } } } @@ -516,31 +204,32 @@ }, "minimist": { "version": "1.1.0", - "from": "minimist@^1.1.0", + "from": "https://registry.npmjs.org/minimist/-/minimist-1.1.0.tgz", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.1.0.tgz" }, "mkdirp": { "version": "0.3.5", - "from": "mkdirp@0.3.5" + "from": "mkdirp@^0.3.5", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.5.tgz" }, "xml2js": { "version": "0.4.4", - "from": "xml2js@^0.4.4", + "from": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.4.tgz", "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.4.tgz", "dependencies": { "sax": { "version": "0.6.1", - "from": "sax@0.6.x", + "from": "https://registry.npmjs.org/sax/-/sax-0.6.1.tgz", "resolved": "https://registry.npmjs.org/sax/-/sax-0.6.1.tgz" }, "xmlbuilder": { "version": "2.4.5", - "from": "xmlbuilder@>=1.0.0", + "from": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-2.4.5.tgz", "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-2.4.5.tgz", "dependencies": { "lodash-node": { "version": "2.4.1", - "from": "lodash-node@~2.4.1", + "from": "https://registry.npmjs.org/lodash-node/-/lodash-node-2.4.1.tgz", "resolved": "https://registry.npmjs.org/lodash-node/-/lodash-node-2.4.1.tgz" } } diff --git a/package.json b/package.json index c2e5b9cb..b098e2ab 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "karma-chrome-launcher": "^0.1.4", "karma-firefox-launcher": "^0.1.3", "karma-jasmine": "^0.1.5", + "karma-coverage": "^0.2.4", "phantomjs": "~1.9.11", "saxon-stream2": "0.0.1", "grunt-http-server": "~1.0.0", @@ -27,7 +28,6 @@ "dependencies": { "express": "^3.4.8", "js-beautify": "^1.5.4", - "karma-coverage": "^0.2.4", "minimist": "^1.1.0", "mkdirp": "^0.3.5", "xml2js": "^0.4.4" From fda63a2ae5de819fda5e17043e2084801d945ea3 Mon Sep 17 00:00:00 2001 From: rikkertkoppes Date: Tue, 25 Nov 2014 19:46:58 +0100 Subject: [PATCH 18/29] Don't use animated opacity, improves scrolling performance, closes #100 --- src/css/elements.css | 15 +++++++-------- src/fllscoring.appcache | 8 ++++---- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/src/css/elements.css b/src/css/elements.css index 43f77f88..49eac83d 100644 --- a/src/css/elements.css +++ b/src/css/elements.css @@ -81,6 +81,7 @@ table { height: 100%; float: left; overflow: auto; + -webkit-overflow-scrolling: touch; overflow-scrolling: touch; } .viewMain.teams { @@ -134,13 +135,13 @@ table { background-color: white; overflow: auto; -webkit-overflow-scrolling: touch; - opacity: 0; + overflow-scrolling: touch; position: absolute; left: 0; top: 0; right: 0; bottom: 0; - -webkit-transition: opacity 0.2s; + display: none; } .viewMain.teams .view-teams, @@ -148,8 +149,7 @@ table { .viewMain.scores .view-scores, .viewMain.ranking .view-ranking, .viewMain.settings .view-settings { - opacity: 1; - z-index: 2; + display: block; } .panel, #teams { @@ -186,22 +186,21 @@ table { background-color: white; overflow: auto; -webkit-overflow-scrolling: touch; - opacity: 0; + overflow-scrolling: touch; position: absolute; left: 0; top: 0; right: 0; bottom: 0; padding: 0 10px; - -webkit-transition: opacity 0.2s; + display: none; } .viewMain.teams .view-teams, .viewMain.scoresheet .view-scoresheet, .viewMain.scores .view-scores, .viewMain.ranking .view-ranking, .viewMain.settings .view-settings { - opacity: 1; - z-index: 2; + display: block; } /*.panel, #teams { diff --git a/src/fllscoring.appcache b/src/fllscoring.appcache index a1721aa1..60456a89 100644 --- a/src/fllscoring.appcache +++ b/src/fllscoring.appcache @@ -1,11 +1,11 @@ CACHE MANIFEST -#2014-11-25 1230 +#2014-11-25 1932 CACHE: index.html -scoring.html +#scoring.html views/main.html -views/mainScoring.html +#views/mainScoring.html views/ranking.html views/scores.html views/scoresheet.html @@ -71,4 +71,4 @@ components/requirejs/require.js components/signature-pad/jquery.signaturepad.min.js NETWORK: -* \ No newline at end of file +* From a5e158b89f4460008c5ac920bf472dd2791ad805 Mon Sep 17 00:00:00 2001 From: rikkertkoppes Date: Tue, 25 Nov 2014 19:47:13 +0100 Subject: [PATCH 19/29] Added nocache version of the index, for development --- src/nocache.html | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 src/nocache.html diff --git a/src/nocache.html b/src/nocache.html new file mode 100644 index 00000000..4c1c6626 --- /dev/null +++ b/src/nocache.html @@ -0,0 +1,23 @@ + + + + + + + + + FLL Scoring + + + + + +
      + + From 7e4dc58079a302274a66001dc105d1bcb33a1b64 Mon Sep 17 00:00:00 2001 From: rikkertkoppes Date: Tue, 25 Nov 2014 20:09:40 +0100 Subject: [PATCH 20/29] include ngTouch in favor of fastclick --- .gitignore | 5 +- bower.json | 4 +- src/components/angular-touch/angular-touch.js | 622 ++++++++++++++ .../angular-touch/angular-touch.min.js | 13 + src/components/fastclick/lib/fastclick.js | 790 ------------------ src/fllscoring.appcache | 2 +- src/js/config.js | 7 +- src/js/main.js | 11 +- 8 files changed, 647 insertions(+), 807 deletions(-) create mode 100644 src/components/angular-touch/angular-touch.js create mode 100644 src/components/angular-touch/angular-touch.min.js delete mode 100644 src/components/fastclick/lib/fastclick.js diff --git a/.gitignore b/.gitignore index 8b37dd85..6c1deba7 100644 --- a/.gitignore +++ b/.gitignore @@ -14,16 +14,13 @@ src/components/*/* !src/components/angular/angular*.js !src/components/angular-sanitize/angular-sanitize*.js !src/components/angular-mocks/angular*.js +!src/components/angular-touch/angular*.js !src/components/angular-bootstrap/*.js !src/components/bootstrap-css/css !src/components/bootstrap-css/img !src/components/bootstrap-css/js - -!src/components/fastclick/lib -!src/components/fastclick/lib/fastclick.js - !src/components/font-awesome/css !src/components/font-awesome/font diff --git a/bower.json b/bower.json index edf429ff..815e5dd2 100644 --- a/bower.json +++ b/bower.json @@ -22,10 +22,10 @@ "font-awesome": "~3.2.1", "idbwrapper": "~1.3.0", "signature-pad": "https://github.com/thomasjbradley/signature-pad/archive/master.zip", - "fastclick": "~1.0.2", "angular": "~1.2.20", "angular-mocks": "~1.2.20", "angular-bootstrap": "~0.11.0", - "angular-sanitize": "~1.3.3" + "angular-sanitize": "~1.3.3", + "angular-touch": "~1.3.4" } } diff --git a/src/components/angular-touch/angular-touch.js b/src/components/angular-touch/angular-touch.js new file mode 100644 index 00000000..7b9ab775 --- /dev/null +++ b/src/components/angular-touch/angular-touch.js @@ -0,0 +1,622 @@ +/** + * @license AngularJS v1.3.4 + * (c) 2010-2014 Google, Inc. http://angularjs.org + * License: MIT + */ +(function(window, angular, undefined) {'use strict'; + +/** + * @ngdoc module + * @name ngTouch + * @description + * + * # ngTouch + * + * The `ngTouch` module provides touch events and other helpers for touch-enabled devices. + * The implementation is based on jQuery Mobile touch event handling + * ([jquerymobile.com](http://jquerymobile.com/)). + * + * + * See {@link ngTouch.$swipe `$swipe`} for usage. + * + *
      + * + */ + +// define ngTouch module +/* global -ngTouch */ +var ngTouch = angular.module('ngTouch', []); + +/* global ngTouch: false */ + + /** + * @ngdoc service + * @name $swipe + * + * @description + * The `$swipe` service is a service that abstracts the messier details of hold-and-drag swipe + * behavior, to make implementing swipe-related directives more convenient. + * + * Requires the {@link ngTouch `ngTouch`} module to be installed. + * + * `$swipe` is used by the `ngSwipeLeft` and `ngSwipeRight` directives in `ngTouch`, and by + * `ngCarousel` in a separate component. + * + * # Usage + * The `$swipe` service is an object with a single method: `bind`. `bind` takes an element + * which is to be watched for swipes, and an object with four handler functions. See the + * documentation for `bind` below. + */ + +ngTouch.factory('$swipe', [function() { + // The total distance in any direction before we make the call on swipe vs. scroll. + var MOVE_BUFFER_RADIUS = 10; + + var POINTER_EVENTS = { + 'mouse': { + start: 'mousedown', + move: 'mousemove', + end: 'mouseup' + }, + 'touch': { + start: 'touchstart', + move: 'touchmove', + end: 'touchend', + cancel: 'touchcancel' + } + }; + + function getCoordinates(event) { + var touches = event.touches && event.touches.length ? event.touches : [event]; + var e = (event.changedTouches && event.changedTouches[0]) || + (event.originalEvent && event.originalEvent.changedTouches && + event.originalEvent.changedTouches[0]) || + touches[0].originalEvent || touches[0]; + + return { + x: e.clientX, + y: e.clientY + }; + } + + function getEvents(pointerTypes, eventType) { + var res = []; + angular.forEach(pointerTypes, function(pointerType) { + var eventName = POINTER_EVENTS[pointerType][eventType]; + if (eventName) { + res.push(eventName); + } + }); + return res.join(' '); + } + + return { + /** + * @ngdoc method + * @name $swipe#bind + * + * @description + * The main method of `$swipe`. It takes an element to be watched for swipe motions, and an + * object containing event handlers. + * The pointer types that should be used can be specified via the optional + * third argument, which is an array of strings `'mouse'` and `'touch'`. By default, + * `$swipe` will listen for `mouse` and `touch` events. + * + * The four events are `start`, `move`, `end`, and `cancel`. `start`, `move`, and `end` + * receive as a parameter a coordinates object of the form `{ x: 150, y: 310 }`. + * + * `start` is called on either `mousedown` or `touchstart`. After this event, `$swipe` is + * watching for `touchmove` or `mousemove` events. These events are ignored until the total + * distance moved in either dimension exceeds a small threshold. + * + * Once this threshold is exceeded, either the horizontal or vertical delta is greater. + * - If the horizontal distance is greater, this is a swipe and `move` and `end` events follow. + * - If the vertical distance is greater, this is a scroll, and we let the browser take over. + * A `cancel` event is sent. + * + * `move` is called on `mousemove` and `touchmove` after the above logic has determined that + * a swipe is in progress. + * + * `end` is called when a swipe is successfully completed with a `touchend` or `mouseup`. + * + * `cancel` is called either on a `touchcancel` from the browser, or when we begin scrolling + * as described above. + * + */ + bind: function(element, eventHandlers, pointerTypes) { + // Absolute total movement, used to control swipe vs. scroll. + var totalX, totalY; + // Coordinates of the start position. + var startCoords; + // Last event's position. + var lastPos; + // Whether a swipe is active. + var active = false; + + pointerTypes = pointerTypes || ['mouse', 'touch']; + element.on(getEvents(pointerTypes, 'start'), function(event) { + startCoords = getCoordinates(event); + active = true; + totalX = 0; + totalY = 0; + lastPos = startCoords; + eventHandlers['start'] && eventHandlers['start'](startCoords, event); + }); + var events = getEvents(pointerTypes, 'cancel'); + if (events) { + element.on(events, function(event) { + active = false; + eventHandlers['cancel'] && eventHandlers['cancel'](event); + }); + } + + element.on(getEvents(pointerTypes, 'move'), function(event) { + if (!active) return; + + // Android will send a touchcancel if it thinks we're starting to scroll. + // So when the total distance (+ or - or both) exceeds 10px in either direction, + // we either: + // - On totalX > totalY, we send preventDefault() and treat this as a swipe. + // - On totalY > totalX, we let the browser handle it as a scroll. + + if (!startCoords) return; + var coords = getCoordinates(event); + + totalX += Math.abs(coords.x - lastPos.x); + totalY += Math.abs(coords.y - lastPos.y); + + lastPos = coords; + + if (totalX < MOVE_BUFFER_RADIUS && totalY < MOVE_BUFFER_RADIUS) { + return; + } + + // One of totalX or totalY has exceeded the buffer, so decide on swipe vs. scroll. + if (totalY > totalX) { + // Allow native scrolling to take over. + active = false; + eventHandlers['cancel'] && eventHandlers['cancel'](event); + return; + } else { + // Prevent the browser from scrolling. + event.preventDefault(); + eventHandlers['move'] && eventHandlers['move'](coords, event); + } + }); + + element.on(getEvents(pointerTypes, 'end'), function(event) { + if (!active) return; + active = false; + eventHandlers['end'] && eventHandlers['end'](getCoordinates(event), event); + }); + } + }; +}]); + +/* global ngTouch: false */ + +/** + * @ngdoc directive + * @name ngClick + * + * @description + * A more powerful replacement for the default ngClick designed to be used on touchscreen + * devices. Most mobile browsers wait about 300ms after a tap-and-release before sending + * the click event. This version handles them immediately, and then prevents the + * following click event from propagating. + * + * Requires the {@link ngTouch `ngTouch`} module to be installed. + * + * This directive can fall back to using an ordinary click event, and so works on desktop + * browsers as well as mobile. + * + * This directive also sets the CSS class `ng-click-active` while the element is being held + * down (by a mouse click or touch) so you can restyle the depressed element if you wish. + * + * @element ANY + * @param {expression} ngClick {@link guide/expression Expression} to evaluate + * upon tap. (Event object is available as `$event`) + * + * @example + + + + count: {{ count }} + + + angular.module('ngClickExample', ['ngTouch']); + + + */ + +ngTouch.config(['$provide', function($provide) { + $provide.decorator('ngClickDirective', ['$delegate', function($delegate) { + // drop the default ngClick directive + $delegate.shift(); + return $delegate; + }]); +}]); + +ngTouch.directive('ngClick', ['$parse', '$timeout', '$rootElement', + function($parse, $timeout, $rootElement) { + var TAP_DURATION = 750; // Shorter than 750ms is a tap, longer is a taphold or drag. + var MOVE_TOLERANCE = 12; // 12px seems to work in most mobile browsers. + var PREVENT_DURATION = 2500; // 2.5 seconds maximum from preventGhostClick call to click + var CLICKBUSTER_THRESHOLD = 25; // 25 pixels in any dimension is the limit for busting clicks. + + var ACTIVE_CLASS_NAME = 'ng-click-active'; + var lastPreventedTime; + var touchCoordinates; + var lastLabelClickCoordinates; + + + // TAP EVENTS AND GHOST CLICKS + // + // Why tap events? + // Mobile browsers detect a tap, then wait a moment (usually ~300ms) to see if you're + // double-tapping, and then fire a click event. + // + // This delay sucks and makes mobile apps feel unresponsive. + // So we detect touchstart, touchmove, touchcancel and touchend ourselves and determine when + // the user has tapped on something. + // + // What happens when the browser then generates a click event? + // The browser, of course, also detects the tap and fires a click after a delay. This results in + // tapping/clicking twice. We do "clickbusting" to prevent it. + // + // How does it work? + // We attach global touchstart and click handlers, that run during the capture (early) phase. + // So the sequence for a tap is: + // - global touchstart: Sets an "allowable region" at the point touched. + // - element's touchstart: Starts a touch + // (- touchmove or touchcancel ends the touch, no click follows) + // - element's touchend: Determines if the tap is valid (didn't move too far away, didn't hold + // too long) and fires the user's tap handler. The touchend also calls preventGhostClick(). + // - preventGhostClick() removes the allowable region the global touchstart created. + // - The browser generates a click event. + // - The global click handler catches the click, and checks whether it was in an allowable region. + // - If preventGhostClick was called, the region will have been removed, the click is busted. + // - If the region is still there, the click proceeds normally. Therefore clicks on links and + // other elements without ngTap on them work normally. + // + // This is an ugly, terrible hack! + // Yeah, tell me about it. The alternatives are using the slow click events, or making our users + // deal with the ghost clicks, so I consider this the least of evils. Fortunately Angular + // encapsulates this ugly logic away from the user. + // + // Why not just put click handlers on the element? + // We do that too, just to be sure. If the tap event caused the DOM to change, + // it is possible another element is now in that position. To take account for these possibly + // distinct elements, the handlers are global and care only about coordinates. + + // Checks if the coordinates are close enough to be within the region. + function hit(x1, y1, x2, y2) { + return Math.abs(x1 - x2) < CLICKBUSTER_THRESHOLD && Math.abs(y1 - y2) < CLICKBUSTER_THRESHOLD; + } + + // Checks a list of allowable regions against a click location. + // Returns true if the click should be allowed. + // Splices out the allowable region from the list after it has been used. + function checkAllowableRegions(touchCoordinates, x, y) { + for (var i = 0; i < touchCoordinates.length; i += 2) { + if (hit(touchCoordinates[i], touchCoordinates[i + 1], x, y)) { + touchCoordinates.splice(i, i + 2); + return true; // allowable region + } + } + return false; // No allowable region; bust it. + } + + // Global click handler that prevents the click if it's in a bustable zone and preventGhostClick + // was called recently. + function onClick(event) { + if (Date.now() - lastPreventedTime > PREVENT_DURATION) { + return; // Too old. + } + + var touches = event.touches && event.touches.length ? event.touches : [event]; + var x = touches[0].clientX; + var y = touches[0].clientY; + // Work around desktop Webkit quirk where clicking a label will fire two clicks (on the label + // and on the input element). Depending on the exact browser, this second click we don't want + // to bust has either (0,0), negative coordinates, or coordinates equal to triggering label + // click event + if (x < 1 && y < 1) { + return; // offscreen + } + if (lastLabelClickCoordinates && + lastLabelClickCoordinates[0] === x && lastLabelClickCoordinates[1] === y) { + return; // input click triggered by label click + } + // reset label click coordinates on first subsequent click + if (lastLabelClickCoordinates) { + lastLabelClickCoordinates = null; + } + // remember label click coordinates to prevent click busting of trigger click event on input + if (event.target.tagName.toLowerCase() === 'label') { + lastLabelClickCoordinates = [x, y]; + } + + // Look for an allowable region containing this click. + // If we find one, that means it was created by touchstart and not removed by + // preventGhostClick, so we don't bust it. + if (checkAllowableRegions(touchCoordinates, x, y)) { + return; + } + + // If we didn't find an allowable region, bust the click. + event.stopPropagation(); + event.preventDefault(); + + // Blur focused form elements + event.target && event.target.blur(); + } + + + // Global touchstart handler that creates an allowable region for a click event. + // This allowable region can be removed by preventGhostClick if we want to bust it. + function onTouchStart(event) { + var touches = event.touches && event.touches.length ? event.touches : [event]; + var x = touches[0].clientX; + var y = touches[0].clientY; + touchCoordinates.push(x, y); + + $timeout(function() { + // Remove the allowable region. + for (var i = 0; i < touchCoordinates.length; i += 2) { + if (touchCoordinates[i] == x && touchCoordinates[i + 1] == y) { + touchCoordinates.splice(i, i + 2); + return; + } + } + }, PREVENT_DURATION, false); + } + + // On the first call, attaches some event handlers. Then whenever it gets called, it creates a + // zone around the touchstart where clicks will get busted. + function preventGhostClick(x, y) { + if (!touchCoordinates) { + $rootElement[0].addEventListener('click', onClick, true); + $rootElement[0].addEventListener('touchstart', onTouchStart, true); + touchCoordinates = []; + } + + lastPreventedTime = Date.now(); + + checkAllowableRegions(touchCoordinates, x, y); + } + + // Actual linking function. + return function(scope, element, attr) { + var clickHandler = $parse(attr.ngClick), + tapping = false, + tapElement, // Used to blur the element after a tap. + startTime, // Used to check if the tap was held too long. + touchStartX, + touchStartY; + + function resetState() { + tapping = false; + element.removeClass(ACTIVE_CLASS_NAME); + } + + element.on('touchstart', function(event) { + tapping = true; + tapElement = event.target ? event.target : event.srcElement; // IE uses srcElement. + // Hack for Safari, which can target text nodes instead of containers. + if (tapElement.nodeType == 3) { + tapElement = tapElement.parentNode; + } + + element.addClass(ACTIVE_CLASS_NAME); + + startTime = Date.now(); + + var touches = event.touches && event.touches.length ? event.touches : [event]; + var e = touches[0].originalEvent || touches[0]; + touchStartX = e.clientX; + touchStartY = e.clientY; + }); + + element.on('touchmove', function(event) { + resetState(); + }); + + element.on('touchcancel', function(event) { + resetState(); + }); + + element.on('touchend', function(event) { + var diff = Date.now() - startTime; + + var touches = (event.changedTouches && event.changedTouches.length) ? event.changedTouches : + ((event.touches && event.touches.length) ? event.touches : [event]); + var e = touches[0].originalEvent || touches[0]; + var x = e.clientX; + var y = e.clientY; + var dist = Math.sqrt(Math.pow(x - touchStartX, 2) + Math.pow(y - touchStartY, 2)); + + if (tapping && diff < TAP_DURATION && dist < MOVE_TOLERANCE) { + // Call preventGhostClick so the clickbuster will catch the corresponding click. + preventGhostClick(x, y); + + // Blur the focused element (the button, probably) before firing the callback. + // This doesn't work perfectly on Android Chrome, but seems to work elsewhere. + // I couldn't get anything to work reliably on Android Chrome. + if (tapElement) { + tapElement.blur(); + } + + if (!angular.isDefined(attr.disabled) || attr.disabled === false) { + element.triggerHandler('click', [event]); + } + } + + resetState(); + }); + + // Hack for iOS Safari's benefit. It goes searching for onclick handlers and is liable to click + // something else nearby. + element.onclick = function(event) { }; + + // Actual click handler. + // There are three different kinds of clicks, only two of which reach this point. + // - On desktop browsers without touch events, their clicks will always come here. + // - On mobile browsers, the simulated "fast" click will call this. + // - But the browser's follow-up slow click will be "busted" before it reaches this handler. + // Therefore it's safe to use this directive on both mobile and desktop. + element.on('click', function(event, touchend) { + scope.$apply(function() { + clickHandler(scope, {$event: (touchend || event)}); + }); + }); + + element.on('mousedown', function(event) { + element.addClass(ACTIVE_CLASS_NAME); + }); + + element.on('mousemove mouseup', function(event) { + element.removeClass(ACTIVE_CLASS_NAME); + }); + + }; +}]); + +/* global ngTouch: false */ + +/** + * @ngdoc directive + * @name ngSwipeLeft + * + * @description + * Specify custom behavior when an element is swiped to the left on a touchscreen device. + * A leftward swipe is a quick, right-to-left slide of the finger. + * Though ngSwipeLeft is designed for touch-based devices, it will work with a mouse click and drag + * too. + * + * To disable the mouse click and drag functionality, add `ng-swipe-disable-mouse` to + * the `ng-swipe-left` or `ng-swipe-right` DOM Element. + * + * Requires the {@link ngTouch `ngTouch`} module to be installed. + * + * @element ANY + * @param {expression} ngSwipeLeft {@link guide/expression Expression} to evaluate + * upon left swipe. (Event object is available as `$event`) + * + * @example + + +
      + Some list content, like an email in the inbox +
      +
      + + +
      +
      + + angular.module('ngSwipeLeftExample', ['ngTouch']); + +
      + */ + +/** + * @ngdoc directive + * @name ngSwipeRight + * + * @description + * Specify custom behavior when an element is swiped to the right on a touchscreen device. + * A rightward swipe is a quick, left-to-right slide of the finger. + * Though ngSwipeRight is designed for touch-based devices, it will work with a mouse click and drag + * too. + * + * Requires the {@link ngTouch `ngTouch`} module to be installed. + * + * @element ANY + * @param {expression} ngSwipeRight {@link guide/expression Expression} to evaluate + * upon right swipe. (Event object is available as `$event`) + * + * @example + + +
      + Some list content, like an email in the inbox +
      +
      + + +
      +
      + + angular.module('ngSwipeRightExample', ['ngTouch']); + +
      + */ + +function makeSwipeDirective(directiveName, direction, eventName) { + ngTouch.directive(directiveName, ['$parse', '$swipe', function($parse, $swipe) { + // The maximum vertical delta for a swipe should be less than 75px. + var MAX_VERTICAL_DISTANCE = 75; + // Vertical distance should not be more than a fraction of the horizontal distance. + var MAX_VERTICAL_RATIO = 0.3; + // At least a 30px lateral motion is necessary for a swipe. + var MIN_HORIZONTAL_DISTANCE = 30; + + return function(scope, element, attr) { + var swipeHandler = $parse(attr[directiveName]); + + var startCoords, valid; + + function validSwipe(coords) { + // Check that it's within the coordinates. + // Absolute vertical distance must be within tolerances. + // Horizontal distance, we take the current X - the starting X. + // This is negative for leftward swipes and positive for rightward swipes. + // After multiplying by the direction (-1 for left, +1 for right), legal swipes + // (ie. same direction as the directive wants) will have a positive delta and + // illegal ones a negative delta. + // Therefore this delta must be positive, and larger than the minimum. + if (!startCoords) return false; + var deltaY = Math.abs(coords.y - startCoords.y); + var deltaX = (coords.x - startCoords.x) * direction; + return valid && // Short circuit for already-invalidated swipes. + deltaY < MAX_VERTICAL_DISTANCE && + deltaX > 0 && + deltaX > MIN_HORIZONTAL_DISTANCE && + deltaY / deltaX < MAX_VERTICAL_RATIO; + } + + var pointerTypes = ['touch']; + if (!angular.isDefined(attr['ngSwipeDisableMouse'])) { + pointerTypes.push('mouse'); + } + $swipe.bind(element, { + 'start': function(coords, event) { + startCoords = coords; + valid = true; + }, + 'cancel': function(event) { + valid = false; + }, + 'end': function(coords, event) { + if (validSwipe(coords)) { + scope.$apply(function() { + element.triggerHandler(eventName); + swipeHandler(scope, {$event: event}); + }); + } + } + }, pointerTypes); + }; + }]); +} + +// Left is negative X-coordinate, right is positive. +makeSwipeDirective('ngSwipeLeft', -1, 'swipeleft'); +makeSwipeDirective('ngSwipeRight', 1, 'swiperight'); + + + +})(window, window.angular); diff --git a/src/components/angular-touch/angular-touch.min.js b/src/components/angular-touch/angular-touch.min.js new file mode 100644 index 00000000..e3533440 --- /dev/null +++ b/src/components/angular-touch/angular-touch.min.js @@ -0,0 +1,13 @@ +/* + AngularJS v1.3.4 + (c) 2010-2014 Google, Inc. http://angularjs.org + License: MIT +*/ +(function(y,u,z){'use strict';function s(h,k,p){n.directive(h,["$parse","$swipe",function(d,e){return function(l,m,f){function g(a){if(!c)return!1;var b=Math.abs(a.y-c.y);a=(a.x-c.x)*k;return q&&75>b&&0b/a}var b=d(f[h]),c,q,a=["touch"];u.isDefined(f.ngSwipeDisableMouse)||a.push("mouse");e.bind(m,{start:function(a,b){c=a;q=!0},cancel:function(a){q=!1},end:function(a,c){g(a)&&l.$apply(function(){m.triggerHandler(p);b(l,{$event:c})})}},a)}}])}var n=u.module("ngTouch",[]);n.factory("$swipe", +[function(){function h(d){var e=d.touches&&d.touches.length?d.touches:[d];d=d.changedTouches&&d.changedTouches[0]||d.originalEvent&&d.originalEvent.changedTouches&&d.originalEvent.changedTouches[0]||e[0].originalEvent||e[0];return{x:d.clientX,y:d.clientY}}function k(d,e){var l=[];u.forEach(d,function(d){(d=p[d][e])&&l.push(d)});return l.join(" ")}var p={mouse:{start:"mousedown",move:"mousemove",end:"mouseup"},touch:{start:"touchstart",move:"touchmove",end:"touchend",cancel:"touchcancel"}};return{bind:function(d, +e,l){var m,f,g,b,c=!1;l=l||["mouse","touch"];d.on(k(l,"start"),function(a){g=h(a);c=!0;f=m=0;b=g;e.start&&e.start(g,a)});var q=k(l,"cancel");if(q)d.on(q,function(a){c=!1;e.cancel&&e.cancel(a)});d.on(k(l,"move"),function(a){if(c&&g){var d=h(a);m+=Math.abs(d.x-b.x);f+=Math.abs(d.y-b.y);b=d;10>m&&10>f||(f>m?(c=!1,e.cancel&&e.cancel(a)):(a.preventDefault(),e.move&&e.move(d,a)))}});d.on(k(l,"end"),function(a){c&&(c=!1,e.end&&e.end(h(a),a))})}}}]);n.config(["$provide",function(h){h.decorator("ngClickDirective", +["$delegate",function(k){k.shift();return k}])}]);n.directive("ngClick",["$parse","$timeout","$rootElement",function(h,k,p){function d(b,c,d){for(var a=0;aMath.abs(b[a]-c)&&25>Math.abs(e-f))return b.splice(a,a+2),!0}return!1}function e(b){if(!(2500e&&1>c||g&&g[0]===e&&g[1]===c||(g&&(g=null),"label"===b.target.tagName.toLowerCase()&&(g=[e,c]),d(f,e,c)||(b.stopPropagation(), +b.preventDefault(),b.target&&b.target.blur()))}}function l(b){b=b.touches&&b.touches.length?b.touches:[b];var c=b[0].clientX,d=b[0].clientY;f.push(c,d);k(function(){for(var a=0;ak&&12>x&&(f||(p[0].addEventListener("click",e,!0),p[0].addEventListener("touchstart", +l,!0),f=[]),m=Date.now(),d(f,h,t),r&&r.blur(),u.isDefined(g.disabled)&&!1!==g.disabled||c.triggerHandler("click",[b]));a()});c.onclick=function(a){};c.on("click",function(a,c){b.$apply(function(){k(b,{$event:c||a})})});c.on("mousedown",function(a){c.addClass("ng-click-active")});c.on("mousemove mouseup",function(a){c.removeClass("ng-click-active")})}}]);s("ngSwipeLeft",-1,"swipeleft");s("ngSwipeRight",1,"swiperight")})(window,window.angular); +//# sourceMappingURL=angular-touch.min.js.map diff --git a/src/components/fastclick/lib/fastclick.js b/src/components/fastclick/lib/fastclick.js deleted file mode 100644 index 05b94b79..00000000 --- a/src/components/fastclick/lib/fastclick.js +++ /dev/null @@ -1,790 +0,0 @@ -/** - * @preserve FastClick: polyfill to remove click delays on browsers with touch UIs. - * - * @version 1.0.2 - * @codingstandard ftlabs-jsv2 - * @copyright The Financial Times Limited [All Rights Reserved] - * @license MIT License (see LICENSE.txt) - */ - -/*jslint browser:true, node:true*/ -/*global define, Event, Node*/ - - -/** - * Instantiate fast-clicking listeners on the specified layer. - * - * @constructor - * @param {Element} layer The layer to listen on - * @param {Object} options The options to override the defaults - */ -function FastClick(layer, options) { - 'use strict'; - var oldOnClick; - - options = options || {}; - - /** - * Whether a click is currently being tracked. - * - * @type boolean - */ - this.trackingClick = false; - - - /** - * Timestamp for when click tracking started. - * - * @type number - */ - this.trackingClickStart = 0; - - - /** - * The element being tracked for a click. - * - * @type EventTarget - */ - this.targetElement = null; - - - /** - * X-coordinate of touch start event. - * - * @type number - */ - this.touchStartX = 0; - - - /** - * Y-coordinate of touch start event. - * - * @type number - */ - this.touchStartY = 0; - - - /** - * ID of the last touch, retrieved from Touch.identifier. - * - * @type number - */ - this.lastTouchIdentifier = 0; - - - /** - * Touchmove boundary, beyond which a click will be cancelled. - * - * @type number - */ - this.touchBoundary = options.touchBoundary || 10; - - - /** - * The FastClick layer. - * - * @type Element - */ - this.layer = layer; - - /** - * The minimum time between tap(touchstart and touchend) events - * - * @type number - */ - this.tapDelay = options.tapDelay || 200; - - if (FastClick.notNeeded(layer)) { - return; - } - - // Some old versions of Android don't have Function.prototype.bind - function bind(method, context) { - return function() { return method.apply(context, arguments); }; - } - - - var methods = ['onMouse', 'onClick', 'onTouchStart', 'onTouchMove', 'onTouchEnd', 'onTouchCancel']; - var context = this; - for (var i = 0, l = methods.length; i < l; i++) { - context[methods[i]] = bind(context[methods[i]], context); - } - - // Set up event handlers as required - if (deviceIsAndroid) { - layer.addEventListener('mouseover', this.onMouse, true); - layer.addEventListener('mousedown', this.onMouse, true); - layer.addEventListener('mouseup', this.onMouse, true); - } - - layer.addEventListener('click', this.onClick, true); - layer.addEventListener('touchstart', this.onTouchStart, false); - layer.addEventListener('touchmove', this.onTouchMove, false); - layer.addEventListener('touchend', this.onTouchEnd, false); - layer.addEventListener('touchcancel', this.onTouchCancel, false); - - // Hack is required for browsers that don't support Event#stopImmediatePropagation (e.g. Android 2) - // which is how FastClick normally stops click events bubbling to callbacks registered on the FastClick - // layer when they are cancelled. - if (!Event.prototype.stopImmediatePropagation) { - layer.removeEventListener = function(type, callback, capture) { - var rmv = Node.prototype.removeEventListener; - if (type === 'click') { - rmv.call(layer, type, callback.hijacked || callback, capture); - } else { - rmv.call(layer, type, callback, capture); - } - }; - - layer.addEventListener = function(type, callback, capture) { - var adv = Node.prototype.addEventListener; - if (type === 'click') { - adv.call(layer, type, callback.hijacked || (callback.hijacked = function(event) { - if (!event.propagationStopped) { - callback(event); - } - }), capture); - } else { - adv.call(layer, type, callback, capture); - } - }; - } - - // If a handler is already declared in the element's onclick attribute, it will be fired before - // FastClick's onClick handler. Fix this by pulling out the user-defined handler function and - // adding it as listener. - if (typeof layer.onclick === 'function') { - - // Android browser on at least 3.2 requires a new reference to the function in layer.onclick - // - the old one won't work if passed to addEventListener directly. - oldOnClick = layer.onclick; - layer.addEventListener('click', function(event) { - oldOnClick(event); - }, false); - layer.onclick = null; - } -} - - -/** - * Android requires exceptions. - * - * @type boolean - */ -var deviceIsAndroid = navigator.userAgent.indexOf('Android') > 0; - - -/** - * iOS requires exceptions. - * - * @type boolean - */ -var deviceIsIOS = /iP(ad|hone|od)/.test(navigator.userAgent); - - -/** - * iOS 4 requires an exception for select elements. - * - * @type boolean - */ -var deviceIsIOS4 = deviceIsIOS && (/OS 4_\d(_\d)?/).test(navigator.userAgent); - - -/** - * iOS 6.0(+?) requires the target element to be manually derived - * - * @type boolean - */ -var deviceIsIOSWithBadTarget = deviceIsIOS && (/OS ([6-9]|\d{2})_\d/).test(navigator.userAgent); - - -/** - * Determine whether a given element requires a native click. - * - * @param {EventTarget|Element} target Target DOM element - * @returns {boolean} Returns true if the element needs a native click - */ -FastClick.prototype.needsClick = function(target) { - 'use strict'; - switch (target.nodeName.toLowerCase()) { - - // Don't send a synthetic click to disabled inputs (issue #62) - case 'button': - case 'select': - case 'textarea': - if (target.disabled) { - return true; - } - - break; - case 'input': - - // File inputs need real clicks on iOS 6 due to a browser bug (issue #68) - if ((deviceIsIOS && target.type === 'file') || target.disabled) { - return true; - } - - break; - case 'label': - case 'video': - return true; - } - - return (/\bneedsclick\b/).test(target.className); -}; - - -/** - * Determine whether a given element requires a call to focus to simulate click into element. - * - * @param {EventTarget|Element} target Target DOM element - * @returns {boolean} Returns true if the element requires a call to focus to simulate native click. - */ -FastClick.prototype.needsFocus = function(target) { - 'use strict'; - switch (target.nodeName.toLowerCase()) { - case 'textarea': - return true; - case 'select': - return !deviceIsAndroid; - case 'input': - switch (target.type) { - case 'button': - case 'checkbox': - case 'file': - case 'image': - case 'radio': - case 'submit': - return false; - } - - // No point in attempting to focus disabled inputs - return !target.disabled && !target.readOnly; - default: - return (/\bneedsfocus\b/).test(target.className); - } -}; - - -/** - * Send a click event to the specified element. - * - * @param {EventTarget|Element} targetElement - * @param {Event} event - */ -FastClick.prototype.sendClick = function(targetElement, event) { - 'use strict'; - var clickEvent, touch; - - // On some Android devices activeElement needs to be blurred otherwise the synthetic click will have no effect (#24) - if (document.activeElement && document.activeElement !== targetElement) { - document.activeElement.blur(); - } - - touch = event.changedTouches[0]; - - // Synthesise a click event, with an extra attribute so it can be tracked - clickEvent = document.createEvent('MouseEvents'); - clickEvent.initMouseEvent(this.determineEventType(targetElement), true, true, window, 1, touch.screenX, touch.screenY, touch.clientX, touch.clientY, false, false, false, false, 0, null); - clickEvent.forwardedTouchEvent = true; - targetElement.dispatchEvent(clickEvent); -}; - -FastClick.prototype.determineEventType = function(targetElement) { - 'use strict'; - - //Issue #159: Android Chrome Select Box does not open with a synthetic click event - if (deviceIsAndroid && targetElement.tagName.toLowerCase() === 'select') { - return 'mousedown'; - } - - return 'click'; -}; - - -/** - * @param {EventTarget|Element} targetElement - */ -FastClick.prototype.focus = function(targetElement) { - 'use strict'; - var length; - - // Issue #160: on iOS 7, some input elements (e.g. date datetime) throw a vague TypeError on setSelectionRange. These elements don't have an integer value for the selectionStart and selectionEnd properties, but unfortunately that can't be used for detection because accessing the properties also throws a TypeError. Just check the type instead. Filed as Apple bug #15122724. - if (deviceIsIOS && targetElement.setSelectionRange && targetElement.type.indexOf('date') !== 0 && targetElement.type !== 'time') { - length = targetElement.value.length; - targetElement.setSelectionRange(length, length); - } else { - targetElement.focus(); - } -}; - - -/** - * Check whether the given target element is a child of a scrollable layer and if so, set a flag on it. - * - * @param {EventTarget|Element} targetElement - */ -FastClick.prototype.updateScrollParent = function(targetElement) { - 'use strict'; - var scrollParent, parentElement; - - scrollParent = targetElement.fastClickScrollParent; - - // Attempt to discover whether the target element is contained within a scrollable layer. Re-check if the - // target element was moved to another parent. - if (!scrollParent || !scrollParent.contains(targetElement)) { - parentElement = targetElement; - do { - if (parentElement.scrollHeight > parentElement.offsetHeight) { - scrollParent = parentElement; - targetElement.fastClickScrollParent = parentElement; - break; - } - - parentElement = parentElement.parentElement; - } while (parentElement); - } - - // Always update the scroll top tracker if possible. - if (scrollParent) { - scrollParent.fastClickLastScrollTop = scrollParent.scrollTop; - } -}; - - -/** - * @param {EventTarget} targetElement - * @returns {Element|EventTarget} - */ -FastClick.prototype.getTargetElementFromEventTarget = function(eventTarget) { - 'use strict'; - - // On some older browsers (notably Safari on iOS 4.1 - see issue #56) the event target may be a text node. - if (eventTarget.nodeType === Node.TEXT_NODE) { - return eventTarget.parentNode; - } - - return eventTarget; -}; - - -/** - * On touch start, record the position and scroll offset. - * - * @param {Event} event - * @returns {boolean} - */ -FastClick.prototype.onTouchStart = function(event) { - 'use strict'; - var targetElement, touch, selection; - - // Ignore multiple touches, otherwise pinch-to-zoom is prevented if both fingers are on the FastClick element (issue #111). - if (event.targetTouches.length > 1) { - return true; - } - - targetElement = this.getTargetElementFromEventTarget(event.target); - touch = event.targetTouches[0]; - - if (deviceIsIOS) { - - // Only trusted events will deselect text on iOS (issue #49) - selection = window.getSelection(); - if (selection.rangeCount && !selection.isCollapsed) { - return true; - } - - if (!deviceIsIOS4) { - - // Weird things happen on iOS when an alert or confirm dialog is opened from a click event callback (issue #23): - // when the user next taps anywhere else on the page, new touchstart and touchend events are dispatched - // with the same identifier as the touch event that previously triggered the click that triggered the alert. - // Sadly, there is an issue on iOS 4 that causes some normal touch events to have the same identifier as an - // immediately preceeding touch event (issue #52), so this fix is unavailable on that platform. - if (touch.identifier === this.lastTouchIdentifier) { - event.preventDefault(); - return false; - } - - this.lastTouchIdentifier = touch.identifier; - - // If the target element is a child of a scrollable layer (using -webkit-overflow-scrolling: touch) and: - // 1) the user does a fling scroll on the scrollable layer - // 2) the user stops the fling scroll with another tap - // then the event.target of the last 'touchend' event will be the element that was under the user's finger - // when the fling scroll was started, causing FastClick to send a click event to that layer - unless a check - // is made to ensure that a parent layer was not scrolled before sending a synthetic click (issue #42). - this.updateScrollParent(targetElement); - } - } - - this.trackingClick = true; - this.trackingClickStart = event.timeStamp; - this.targetElement = targetElement; - - this.touchStartX = touch.pageX; - this.touchStartY = touch.pageY; - - // Prevent phantom clicks on fast double-tap (issue #36) - if ((event.timeStamp - this.lastClickTime) < this.tapDelay) { - event.preventDefault(); - } - - return true; -}; - - -/** - * Based on a touchmove event object, check whether the touch has moved past a boundary since it started. - * - * @param {Event} event - * @returns {boolean} - */ -FastClick.prototype.touchHasMoved = function(event) { - 'use strict'; - var touch = event.changedTouches[0], boundary = this.touchBoundary; - - if (Math.abs(touch.pageX - this.touchStartX) > boundary || Math.abs(touch.pageY - this.touchStartY) > boundary) { - return true; - } - - return false; -}; - - -/** - * Update the last position. - * - * @param {Event} event - * @returns {boolean} - */ -FastClick.prototype.onTouchMove = function(event) { - 'use strict'; - if (!this.trackingClick) { - return true; - } - - // If the touch has moved, cancel the click tracking - if (this.targetElement !== this.getTargetElementFromEventTarget(event.target) || this.touchHasMoved(event)) { - this.trackingClick = false; - this.targetElement = null; - } - - return true; -}; - - -/** - * Attempt to find the labelled control for the given label element. - * - * @param {EventTarget|HTMLLabelElement} labelElement - * @returns {Element|null} - */ -FastClick.prototype.findControl = function(labelElement) { - 'use strict'; - - // Fast path for newer browsers supporting the HTML5 control attribute - if (labelElement.control !== undefined) { - return labelElement.control; - } - - // All browsers under test that support touch events also support the HTML5 htmlFor attribute - if (labelElement.htmlFor) { - return document.getElementById(labelElement.htmlFor); - } - - // If no for attribute exists, attempt to retrieve the first labellable descendant element - // the list of which is defined here: http://www.w3.org/TR/html5/forms.html#category-label - return labelElement.querySelector('button, input:not([type=hidden]), keygen, meter, output, progress, select, textarea'); -}; - - -/** - * On touch end, determine whether to send a click event at once. - * - * @param {Event} event - * @returns {boolean} - */ -FastClick.prototype.onTouchEnd = function(event) { - 'use strict'; - var forElement, trackingClickStart, targetTagName, scrollParent, touch, targetElement = this.targetElement; - - if (!this.trackingClick) { - return true; - } - - // Prevent phantom clicks on fast double-tap (issue #36) - if ((event.timeStamp - this.lastClickTime) < this.tapDelay) { - this.cancelNextClick = true; - return true; - } - - // Reset to prevent wrong click cancel on input (issue #156). - this.cancelNextClick = false; - - this.lastClickTime = event.timeStamp; - - trackingClickStart = this.trackingClickStart; - this.trackingClick = false; - this.trackingClickStart = 0; - - // On some iOS devices, the targetElement supplied with the event is invalid if the layer - // is performing a transition or scroll, and has to be re-detected manually. Note that - // for this to function correctly, it must be called *after* the event target is checked! - // See issue #57; also filed as rdar://13048589 . - if (deviceIsIOSWithBadTarget) { - touch = event.changedTouches[0]; - - // In certain cases arguments of elementFromPoint can be negative, so prevent setting targetElement to null - targetElement = document.elementFromPoint(touch.pageX - window.pageXOffset, touch.pageY - window.pageYOffset) || targetElement; - targetElement.fastClickScrollParent = this.targetElement.fastClickScrollParent; - } - - targetTagName = targetElement.tagName.toLowerCase(); - if (targetTagName === 'label') { - forElement = this.findControl(targetElement); - if (forElement) { - this.focus(targetElement); - if (deviceIsAndroid) { - return false; - } - - targetElement = forElement; - } - } else if (this.needsFocus(targetElement)) { - - // Case 1: If the touch started a while ago (best guess is 100ms based on tests for issue #36) then focus will be triggered anyway. Return early and unset the target element reference so that the subsequent click will be allowed through. - // Case 2: Without this exception for input elements tapped when the document is contained in an iframe, then any inputted text won't be visible even though the value attribute is updated as the user types (issue #37). - if ((event.timeStamp - trackingClickStart) > 100 || (deviceIsIOS && window.top !== window && targetTagName === 'input')) { - this.targetElement = null; - return false; - } - - this.focus(targetElement); - this.sendClick(targetElement, event); - - // Select elements need the event to go through on iOS 4, otherwise the selector menu won't open. - // Also this breaks opening selects when VoiceOver is active on iOS6, iOS7 (and possibly others) - if (!deviceIsIOS || targetTagName !== 'select') { - this.targetElement = null; - event.preventDefault(); - } - - return false; - } - - if (deviceIsIOS && !deviceIsIOS4) { - - // Don't send a synthetic click event if the target element is contained within a parent layer that was scrolled - // and this tap is being used to stop the scrolling (usually initiated by a fling - issue #42). - scrollParent = targetElement.fastClickScrollParent; - if (scrollParent && scrollParent.fastClickLastScrollTop !== scrollParent.scrollTop) { - return true; - } - } - - // Prevent the actual click from going though - unless the target node is marked as requiring - // real clicks or if it is in the whitelist in which case only non-programmatic clicks are permitted. - if (!this.needsClick(targetElement)) { - event.preventDefault(); - this.sendClick(targetElement, event); - } - - return false; -}; - - -/** - * On touch cancel, stop tracking the click. - * - * @returns {void} - */ -FastClick.prototype.onTouchCancel = function() { - 'use strict'; - this.trackingClick = false; - this.targetElement = null; -}; - - -/** - * Determine mouse events which should be permitted. - * - * @param {Event} event - * @returns {boolean} - */ -FastClick.prototype.onMouse = function(event) { - 'use strict'; - - // If a target element was never set (because a touch event was never fired) allow the event - if (!this.targetElement) { - return true; - } - - if (event.forwardedTouchEvent) { - return true; - } - - // Programmatically generated events targeting a specific element should be permitted - if (!event.cancelable) { - return true; - } - - // Derive and check the target element to see whether the mouse event needs to be permitted; - // unless explicitly enabled, prevent non-touch click events from triggering actions, - // to prevent ghost/doubleclicks. - if (!this.needsClick(this.targetElement) || this.cancelNextClick) { - - // Prevent any user-added listeners declared on FastClick element from being fired. - if (event.stopImmediatePropagation) { - event.stopImmediatePropagation(); - } else { - - // Part of the hack for browsers that don't support Event#stopImmediatePropagation (e.g. Android 2) - event.propagationStopped = true; - } - - // Cancel the event - event.stopPropagation(); - event.preventDefault(); - - return false; - } - - // If the mouse event is permitted, return true for the action to go through. - return true; -}; - - -/** - * On actual clicks, determine whether this is a touch-generated click, a click action occurring - * naturally after a delay after a touch (which needs to be cancelled to avoid duplication), or - * an actual click which should be permitted. - * - * @param {Event} event - * @returns {boolean} - */ -FastClick.prototype.onClick = function(event) { - 'use strict'; - var permitted; - - // It's possible for another FastClick-like library delivered with third-party code to fire a click event before FastClick does (issue #44). In that case, set the click-tracking flag back to false and return early. This will cause onTouchEnd to return early. - if (this.trackingClick) { - this.targetElement = null; - this.trackingClick = false; - return true; - } - - // Very odd behaviour on iOS (issue #18): if a submit element is present inside a form and the user hits enter in the iOS simulator or clicks the Go button on the pop-up OS keyboard the a kind of 'fake' click event will be triggered with the submit-type input element as the target. - if (event.target.type === 'submit' && event.detail === 0) { - return true; - } - - permitted = this.onMouse(event); - - // Only unset targetElement if the click is not permitted. This will ensure that the check for !targetElement in onMouse fails and the browser's click doesn't go through. - if (!permitted) { - this.targetElement = null; - } - - // If clicks are permitted, return true for the action to go through. - return permitted; -}; - - -/** - * Remove all FastClick's event listeners. - * - * @returns {void} - */ -FastClick.prototype.destroy = function() { - 'use strict'; - var layer = this.layer; - - if (deviceIsAndroid) { - layer.removeEventListener('mouseover', this.onMouse, true); - layer.removeEventListener('mousedown', this.onMouse, true); - layer.removeEventListener('mouseup', this.onMouse, true); - } - - layer.removeEventListener('click', this.onClick, true); - layer.removeEventListener('touchstart', this.onTouchStart, false); - layer.removeEventListener('touchmove', this.onTouchMove, false); - layer.removeEventListener('touchend', this.onTouchEnd, false); - layer.removeEventListener('touchcancel', this.onTouchCancel, false); -}; - - -/** - * Check whether FastClick is needed. - * - * @param {Element} layer The layer to listen on - */ -FastClick.notNeeded = function(layer) { - 'use strict'; - var metaViewport; - var chromeVersion; - - // Devices that don't support touch don't need FastClick - if (typeof window.ontouchstart === 'undefined') { - return true; - } - - // Chrome version - zero for other browsers - chromeVersion = +(/Chrome\/([0-9]+)/.exec(navigator.userAgent) || [,0])[1]; - - if (chromeVersion) { - - if (deviceIsAndroid) { - metaViewport = document.querySelector('meta[name=viewport]'); - - if (metaViewport) { - // Chrome on Android with user-scalable="no" doesn't need FastClick (issue #89) - if (metaViewport.content.indexOf('user-scalable=no') !== -1) { - return true; - } - // Chrome 32 and above with width=device-width or less don't need FastClick - if (chromeVersion > 31 && document.documentElement.scrollWidth <= window.outerWidth) { - return true; - } - } - - // Chrome desktop doesn't need FastClick (issue #15) - } else { - return true; - } - } - - // IE10 with -ms-touch-action: none, which disables double-tap-to-zoom (issue #97) - if (layer.style.msTouchAction === 'none') { - return true; - } - - return false; -}; - - -/** - * Factory method for creating a FastClick object - * - * @param {Element} layer The layer to listen on - * @param {Object} options The options to override the defaults - */ -FastClick.attach = function(layer, options) { - 'use strict'; - return new FastClick(layer, options); -}; - - -if (typeof define == 'function' && typeof define.amd == 'object' && define.amd) { - - // AMD. Register as an anonymous module. - define(function() { - 'use strict'; - return FastClick; - }); -} else if (typeof module !== 'undefined' && module.exports) { - module.exports = FastClick.attach; - module.exports.FastClick = FastClick; -} else { - window.FastClick = FastClick; -} diff --git a/src/fllscoring.appcache b/src/fllscoring.appcache index 60456a89..d6d79c30 100644 --- a/src/fllscoring.appcache +++ b/src/fllscoring.appcache @@ -60,10 +60,10 @@ js/views/teams.js components/angular/angular.min.js components/angular-bootstrap/ui-bootstrap-tpls.js components/angular-sanitize/angular-sanitize.min.js +components/angular-touch/angular-touch.min.js components/bootstrap-css/css/bootstrap.min.css #components/bootstrap-css/fonts/glyphicons-halflings-regular.woff #components/bootstrap-css/fonts/glyphicons-halflings-regular.ttf -components/fastclick/lib/fastclick.js components/font-awesome/css/font-awesome.min.css components/jquery/jquery.min.js components/q/q.js diff --git a/src/js/config.js b/src/js/config.js index 26407c24..41b6e69e 100644 --- a/src/js/config.js +++ b/src/js/config.js @@ -8,10 +8,10 @@ var require = { 'jquery': '../components/jquery/jquery.min', 'angular': '../components/angular/angular.min', 'angular-sanitize': '../components/angular-sanitize/angular-sanitize.min', + 'angular-touch': '../components/angular-touch/angular-touch.min', 'angular-bootstrap': '../components/angular-bootstrap/ui-bootstrap-tpls', 'idbstore':'../components/idbwrapper/idbstore', - 'signaturepad':'../components/signature-pad/jquery.signaturepad.min', - 'fastclick':'../components/fastclick/lib/fastclick' + 'signaturepad':'../components/signature-pad/jquery.signaturepad.min' }, shim: { 'signaturepad': { @@ -23,6 +23,9 @@ var require = { 'angular-bootstrap': { deps: ['angular'] }, + 'angular-touch': { + deps: ['angular'] + }, 'angular-sanitize': { deps: ['angular'] } diff --git a/src/js/main.js b/src/js/main.js index d84cc4ff..f8f01f7f 100644 --- a/src/js/main.js +++ b/src/js/main.js @@ -1,5 +1,4 @@ define([ - 'fastclick', 'services/log', 'views/settings', 'views/teams', @@ -14,21 +13,16 @@ define([ 'tests/fsTest', 'tests/indexedDBTest', 'angular-bootstrap', + 'angular-touch', 'angular-sanitize', 'angular' -],function(FastClick,log,settings,teams,scoresheet,scores,ranking,services,directives,size,filters,indexFilter,fsTest,dbTest) { +],function(log,settings,teams,scoresheet,scores,ranking,services,directives,size,filters,indexFilter,fsTest,dbTest) { log('device ready'); // fsTest(); // dbTest(); - //initiate fastclick - $(function() { - FastClick.attach(document.body); - }); - - //initialize main controller and load main view //load other main views to create dynamic views for different device layouts angular.module('main',[]).controller('mainCtrl',[ @@ -70,6 +64,7 @@ define([ 'main', 'ui.bootstrap', 'ngSanitize', + 'ngTouch', settings.name, teams.name, scoresheet.name, From 064fcf21e93d7d4fe363aade36fa88c37e201cfd Mon Sep 17 00:00:00 2001 From: rikkertkoppes Date: Tue, 25 Nov 2014 20:28:47 +0100 Subject: [PATCH 21/29] readability + performance, functions outside loop --- src/js/main.js | 2 +- src/js/services/ng-scores.js | 32 +++++++++++++++++++------------- 2 files changed, 20 insertions(+), 14 deletions(-) diff --git a/src/js/main.js b/src/js/main.js index f8f01f7f..04b5afb8 100644 --- a/src/js/main.js +++ b/src/js/main.js @@ -40,7 +40,7 @@ define([ $scope.setPlatform = function(platform) { $scope.platform = platform; - } + }; $scope.containerClass = function(w,h) { w = w(); diff --git a/src/js/services/ng-scores.js b/src/js/services/ng-scores.js index 5e6142bf..1c42eb95 100644 --- a/src/js/services/ng-scores.js +++ b/src/js/services/ng-scores.js @@ -520,6 +520,21 @@ define('services/ng-scores',[ return result; } + function createSortedScores(teamEntry) { + teamEntry.sortedScores = teamEntry.scores.slice(0); // create a copy + teamEntry.sortedScores.sort(scoreCompare); + teamEntry.highest = teamEntry.sortedScores[0]; + } + + function calculateRank(state,teamEntry) { + if (state.lastScores === null || scoresCompare(state.lastScores, teamEntry.sortedScores) !== 0) { + state.rank++; + } + state.lastScores = teamEntry.sortedScores; + teamEntry.rank = state.rank; + return state; + } + // Sort by scores and compute rankings for (var stageId in board) { if (!board.hasOwnProperty(stageId)) { @@ -528,24 +543,15 @@ define('services/ng-scores',[ var stage = board[stageId]; // Create sorted scores and compute highest score per team - stage.forEach(function(teamEntry) { - teamEntry.sortedScores = teamEntry.scores.slice(0); // create a copy - teamEntry.sortedScores.sort(scoreCompare); - teamEntry.highest = teamEntry.sortedScores[0]; - }); + stage.forEach(createSortedScores); // Sort teams based on sorted scores stage.sort(entryCompare); // Compute ranking, assigning equal rank to equal scores - var rank = 0; - var lastScores = null; - stage.forEach(function(teamEntry) { - if (lastScores === null || scoresCompare(lastScores, teamEntry.sortedScores) !== 0) { - rank++; - } - lastScores = teamEntry.sortedScores; - teamEntry.rank = rank; + stage.reduce(calculateRank,{ + rank: 0, + lastScores: null }); } From 5a3dc3d52c707a5212388e29a3306a73c5ef9cd4 Mon Sep 17 00:00:00 2001 From: rikkertkoppes Date: Tue, 25 Nov 2014 20:37:07 +0100 Subject: [PATCH 22/29] removed dependency on $scores in main --- src/js/main.js | 10 +++++++--- src/js/services/ng-scores.js | 14 ++++++++++++-- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/src/js/main.js b/src/js/main.js index 04b5afb8..7ed71134 100644 --- a/src/js/main.js +++ b/src/js/main.js @@ -26,13 +26,17 @@ define([ //initialize main controller and load main view //load other main views to create dynamic views for different device layouts angular.module('main',[]).controller('mainCtrl',[ - '$scope', '$scores', - function($scope, $scores) { + '$scope', + function($scope) { log('init main ctrl'); $scope.mainView = 'views/main.html'; $scope.pages = ['teams','scoresheet','scores','ranking','settings']; $scope.currentPage = $scope.pages[1]; - $scope.validationErrors = $scores.validationErrors; + $scope.validationErrors = []; + + $scope.$on('validationError',function(e,validationErrors) { + $scope.validationErrors = validationErrors; + }); $scope.setPage = function(page) { $scope.currentPage = page; diff --git a/src/js/services/ng-scores.js b/src/js/services/ng-scores.js index 1c42eb95..dfac8457 100644 --- a/src/js/services/ng-scores.js +++ b/src/js/services/ng-scores.js @@ -349,6 +349,16 @@ define('services/ng-scores',[ }); }; + function clearValidationErrors() { + this.validationErrors.splice(0, this.validationErrors.length); + $rootScope.$broadcast('validationError',this.validationErrors); + } + + function addValidationError(error) { + this.validationErrors.push(error); + $rootScope.$broadcast('validationError',this.validationErrors); + } + Scores.prototype._update = function() { if (this._updating > 0) { return; @@ -359,7 +369,7 @@ define('services/ng-scores',[ // Clear existing properties this.scores.splice(0, this.scores.length); // clear without creating new object - this.validationErrors.splice(0, this.validationErrors.length); + clearValidationErrors.call(this); var k; for (k in board) { if (!board.hasOwnProperty(k)) { @@ -558,7 +568,7 @@ define('services/ng-scores',[ // Update validation errors (useful for views) this.scores.forEach(function(score) { if (score.error) { - self.validationErrors.push(score.error); + addValidationError.call(self,score.error); } }); }; From 2c8d321d742145a754607513dbeef209ef2dde26 Mon Sep 17 00:00:00 2001 From: rikkertkoppes Date: Tue, 25 Nov 2014 20:39:22 +0100 Subject: [PATCH 23/29] Add scoring interface to appcache --- src/fllscoring.appcache | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/fllscoring.appcache b/src/fllscoring.appcache index d6d79c30..f5189de6 100644 --- a/src/fllscoring.appcache +++ b/src/fllscoring.appcache @@ -3,9 +3,9 @@ CACHE MANIFEST CACHE: index.html -#scoring.html +scoring.html views/main.html -#views/mainScoring.html +views/mainScoring.html views/ranking.html views/scores.html views/scoresheet.html From 92365dd5c9489af2c5f5dbd194b97cbfea0fb2cd Mon Sep 17 00:00:00 2001 From: rikkertkoppes Date: Tue, 25 Nov 2014 20:43:43 +0100 Subject: [PATCH 24/29] use appcache in scoring view --- src/scoring.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/scoring.html b/src/scoring.html index 057e3069..4321ab51 100644 --- a/src/scoring.html +++ b/src/scoring.html @@ -1,5 +1,5 @@ - + From 8ad60641acdba1d154161ee525cf11e60b94aec0 Mon Sep 17 00:00:00 2001 From: Martin Poelstra Date: Wed, 26 Nov 2014 20:51:28 +0100 Subject: [PATCH 25/29] $scores: Emit all validation errors once, fixes #111. --- src/js/services/ng-scores.js | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/src/js/services/ng-scores.js b/src/js/services/ng-scores.js index dfac8457..f5404d7d 100644 --- a/src/js/services/ng-scores.js +++ b/src/js/services/ng-scores.js @@ -349,16 +349,6 @@ define('services/ng-scores',[ }); }; - function clearValidationErrors() { - this.validationErrors.splice(0, this.validationErrors.length); - $rootScope.$broadcast('validationError',this.validationErrors); - } - - function addValidationError(error) { - this.validationErrors.push(error); - $rootScope.$broadcast('validationError',this.validationErrors); - } - Scores.prototype._update = function() { if (this._updating > 0) { return; @@ -369,7 +359,6 @@ define('services/ng-scores',[ // Clear existing properties this.scores.splice(0, this.scores.length); // clear without creating new object - clearValidationErrors.call(this); var k; for (k in board) { if (!board.hasOwnProperty(k)) { @@ -566,11 +555,13 @@ define('services/ng-scores',[ } // Update validation errors (useful for views) + this.validationErrors.splice(0, this.validationErrors.length); this.scores.forEach(function(score) { if (score.error) { - addValidationError.call(self,score.error); + self.validationErrors.push(score.error); } }); + $rootScope.$broadcast('validationError', this.validationErrors); }; return new Scores(); From 671b6fb9d441b0b933f2916bebab5140c8fd5978 Mon Sep 17 00:00:00 2001 From: Martin Poelstra Date: Wed, 26 Nov 2014 21:35:57 +0100 Subject: [PATCH 26/29] scores: Allow editing score entry when team number is invalid, perform better sanitization in $scores.update(), fixes #105. --- spec/services/ng-scoresSpec.js | 1 + src/js/services/ng-scores.js | 41 +++++++++++++++++++--------------- src/js/views/scores.js | 19 ++++++++-------- 3 files changed, 33 insertions(+), 28 deletions(-) diff --git a/spec/services/ng-scoresSpec.js b/spec/services/ng-scoresSpec.js index 48eab6b7..13495f9c 100644 --- a/spec/services/ng-scoresSpec.js +++ b/spec/services/ng-scoresSpec.js @@ -146,6 +146,7 @@ describe('ng-scores',function() { $scores.update(0, mockScore); expect($scores.scores[0].score).toEqual(151); expect($scores.scores[0].modified).toBeTruthy(); + expect($scores.scores[0].edited).toBeTruthy(); }); }); diff --git a/src/js/services/ng-scores.js b/src/js/services/ng-scores.js index f5404d7d..7add395f 100644 --- a/src/js/services/ng-scores.js +++ b/src/js/services/ng-scores.js @@ -229,17 +229,26 @@ define('services/ng-scores',[ this._update(); }; + function sanitize(score) { + // Convert 'dirty' input score to a representation that we can store + // on a filesystem. This means e.g. not storing denormalized version of + // team and stage, but only their ID's. Additionally, forces values to be + // of the right type where possible. + return { + file: (score.file !== undefined && score.file !== null) ? String(score.file) : "", + teamNumber: parseInt((score.teamNumber !== undefined) ? score.teamNumber : score.team.number, 10), + stageId: String((score.stageId !== undefined) ? score.stageId : score.stage.id), + round: parseInt(score.round, 10), + score: score.score, // can be Number, null, "dnc", etc. + originalScore: parseInt(score.originalScore !== undefined ? score.originalScore : score.score, 10), + edited: score.edited !== undefined ? String(score.edited) : undefined // timestamp, e.g. "Wed Nov 26 2014 21:11:43 GMT+0100 (CET)" + }; + } + Scores.prototype.add = function(score) { // Create a copy of the score, in case the // original score is being modified... - this._rawScores.push({ - file: score.file, - teamNumber: parseInt((score.teamNumber !== undefined) ? score.teamNumber : score.team.number, 10), - stageId: (score.stageId !== undefined) ? score.stageId : score.stage.id, - round: score.round, - score: score.score, - originalScore: score.originalScore !== undefined ? score.originalScore : score.score - }); + this._rawScores.push(sanitize(score)); this._update(); }; @@ -250,18 +259,12 @@ define('services/ng-scores',[ * the score as modified. */ Scores.prototype.update = function(index, score) { - var old = this._rawScores[index]; - if (!old) { + if (index < 0 || index >= this._rawScores.length) { throw new RangeError("unknown score index: " + index); } - // Note: we leave eg. originalScore intact, so _update() will - // mark it as modified. - old.file = score.file; - old.teamNumber = parseInt((score.teamNumber !== undefined) ? score.teamNumber : score.team.number, 10); - old.stageId = (score.stageId !== undefined) ? score.stageId : score.stage.id; - old.round = score.round; - old.score = score.score; - old.edited = (new Date()).toString(); + var newScore = sanitize(score); + newScore.edited = (new Date()).toString(); + this._rawScores.splice(index, 1, newScore); this._update(); }; @@ -380,11 +383,13 @@ define('services/ng-scores',[ // additional info var s = { file: _score.file, + teamNumber: _score.teamNumber, team: $teams.get(_score.teamNumber), stage: $stages.get(_score.stageId), round: _score.round, score: _score.score, originalScore: _score.originalScore, + edited: _score.edited, modified: false, error: null }; diff --git a/src/js/views/scores.js b/src/js/views/scores.js index d7cbf4cf..0ff18a5b 100644 --- a/src/js/views/scores.js +++ b/src/js/views/scores.js @@ -25,22 +25,21 @@ define('views/scores',[ }; $scope.editScore = function(index) { var score = $scores.scores[index]; - score.teamNumber = score.team.number; score.$editing = true; }; $scope.finishEditScore = function(index) { + // The score entry is edited 'inline', then used to + // replace the entry in the scores list and its storage. + // Because scores are always 'sanitized' before storing, + // the $editing flag is automatically discarded. var score = $scores.scores[index]; - score.team = $teams.get(score.teamNumber); - if (!score.team) { - alert('Team number not found'); - return; + try { + $scores.update(score.index, score); + $scores.save(); + } catch(e) { + alert("Error updating score: " + e); } - score.round = parseInt(score.round,10); - score.score = parseInt(score.score,10); - delete score.$editing; - $scores.update(score.index,score); - $scores.save(); }; $scope.cancelEditScore = function() { From e71d2c481d81629f6cd69d2863b2c5dd8dff2f41 Mon Sep 17 00:00:00 2001 From: Martin Poelstra Date: Wed, 26 Nov 2014 22:22:06 +0100 Subject: [PATCH 27/29] scores: Implement editing stage, closes #106. --- spec/views/scoresSpec.js | 3 ++- src/js/services/ng-scores.js | 1 + src/js/views/scores.js | 5 +++-- src/views/scores.html | 7 ++++++- 4 files changed, 12 insertions(+), 4 deletions(-) diff --git a/spec/views/scoresSpec.js b/spec/views/scoresSpec.js index d42ac50c..9367b12d 100644 --- a/spec/views/scoresSpec.js +++ b/spec/views/scoresSpec.js @@ -15,7 +15,8 @@ describe('scores', function() { controller = $controller('scoresCtrl', { '$scope': $scope, '$scores': scoresMock, - '$teams': teamsMock + '$teams': teamsMock, + '$stages': stagesMock }); }); }); diff --git a/src/js/services/ng-scores.js b/src/js/services/ng-scores.js index 7add395f..3cf26013 100644 --- a/src/js/services/ng-scores.js +++ b/src/js/services/ng-scores.js @@ -385,6 +385,7 @@ define('services/ng-scores',[ file: _score.file, teamNumber: _score.teamNumber, team: $teams.get(_score.teamNumber), + stageId: _score.stageId, stage: $stages.get(_score.stageId), round: _score.round, score: _score.score, diff --git a/src/js/views/scores.js b/src/js/views/scores.js index 0ff18a5b..303a777c 100644 --- a/src/js/views/scores.js +++ b/src/js/views/scores.js @@ -6,14 +6,15 @@ define('views/scores',[ ],function(log) { var moduleName = 'scores'; return angular.module(moduleName,[]).controller(moduleName+'Ctrl',[ - '$scope', '$scores','$teams', - function($scope,$scores,$teams) { + '$scope', '$scores','$teams','$stages', + function($scope,$scores,$teams,$stages) { log('init scores ctrl'); $scope.sort = 'index'; $scope.rev = true; $scope.scores = $scores.scores; + $scope.stages = $stages.stages; $scope.doSort = function(col, defaultSort) { $scope.rev = (String($scope.sort) === String(col)) ? !$scope.rev : defaultSort; diff --git a/src/views/scores.html b/src/views/scores.html index ac19a468..2841bd5b 100644 --- a/src/views/scores.html +++ b/src/views/scores.html @@ -19,7 +19,12 @@ {{score.index + 1}} - {{score.stage.name}} + + {{score.stage.name}} + + + + From 3179e537793ee5d0005f884c228ef19e8c53d6e4 Mon Sep 17 00:00:00 2001 From: Martin Poelstra Date: Wed, 26 Nov 2014 22:39:24 +0100 Subject: [PATCH 28/29] $scores: Mark *all* duplicates of a score entry as such. --- src/js/services/ng-scores.js | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/js/services/ng-scores.js b/src/js/services/ng-scores.js index 3cf26013..4f432cc4 100644 --- a/src/js/services/ng-scores.js +++ b/src/js/services/ng-scores.js @@ -446,25 +446,36 @@ define('services/ng-scores',[ } if (!bteam) { var initialScores = new Array(s.stage.rounds); + var initialEntries = new Array(s.stage.rounds); for (i = 0; i < s.stage.rounds; i++) { initialScores[i] = null; + initialEntries[i] = null; } bteam = { team: s.team, scores: initialScores, rank: null, highest: null, + entries: initialEntries, }; bstage.push(bteam); } // Add score to team's entry if (bteam.scores[s.round - 1] !== null) { - // TODO: mark other scores too - s.error = new DuplicateScoreError(bteam.team, s.stage, s.round); + // Find the original entry with which this entry collides, + // then assign an error to that entry and to ourselves. + var dupEntry = bteam.entries[s.round - 1]; + var e = dupEntry.error; + if (!e) { + e = new DuplicateScoreError(bteam.team, s.stage, s.round); + dupEntry.error = e; + } + s.error = e; return; } bteam.scores[s.round - 1] = s.score; + bteam.entries[s.round - 1] = s; }); // Compares two scores. From f17d64aefaa509363c7854e8a49527cf130c716b Mon Sep 17 00:00:00 2001 From: rikkertkoppes Date: Fri, 28 Nov 2014 07:50:51 +0100 Subject: [PATCH 29/29] Make signature not required --- src/js/views/scoresheet.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/js/views/scoresheet.js b/src/js/views/scoresheet.js index 899f38d3..f88b3769 100644 --- a/src/js/views/scoresheet.js +++ b/src/js/views/scoresheet.js @@ -131,7 +131,7 @@ define('views/scoresheet',[ $scope.stage !== undefined && $scope.stage !== null && $scope.round !== undefined && $scope.round !== null && $scope.team !== undefined && $scope.team !== null && - $scope.signature !== undefined && $scope.signature !== null && + // $scope.signature !== undefined && $scope.signature !== null && $scope.missions.every(function(mission) { return mission.objectives.every(function(objective) { return objective.value !== undefined && objective.value !== null;