diff --git a/.gitignore b/.gitignore index b91992ab..06a670ab 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,8 @@ /.idea /.project *.iml +node_modules +import_test +docs-importer-linux +docs-importer-macos +docs-importer-win.exe \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index 51c40faf..30fe4095 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,10 +2,22 @@ sudo: required dist: trusty language: java before_install: + - sudo add-apt-repository -y ppa:mc3man/trusty-media - sudo apt-get -qq update - - sudo apt-get -y -q install tesseract-ocr tesseract-ocr-fra tesseract-ocr-jpn + - sudo apt-get -y -q install ffmpeg mediainfo tesseract-ocr tesseract-ocr-fra tesseract-ocr-ita tesseract-ocr-kor tesseract-ocr-rus tesseract-ocr-ukr tesseract-ocr-spa tesseract-ocr-ara tesseract-ocr-hin tesseract-ocr-deu tesseract-ocr-pol tesseract-ocr-jpn tesseract-ocr-por tesseract-ocr-tha tesseract-ocr-jpn tesseract-ocr-chi-sim tesseract-ocr-chi-tra - sudo apt-get -y -q install haveged && sudo service haveged start +after_success: + - mvn -Pprod -DskipTests clean install + - docker login -u $DOCKER_USER -p $DOCKER_PASS + - export REPO=sismics/docs + - export TAG=`if [ "$TRAVIS_BRANCH" == "master" ]; then echo "latest"; else echo $TRAVIS_BRANCH ; fi` + - docker build -f Dockerfile -t $REPO:$COMMIT . + - docker tag $REPO:$COMMIT $REPO:$TAG + - docker tag $REPO:$COMMIT $REPO:travis-$TRAVIS_BUILD_NUMBER + - docker push $REPO env: global: - - TESSDATA_PREFIX=/usr/share/tesseract-ocr - - LC_NUMERIC=C \ No newline at end of file + - secure: LRGpjWORb0qy6VuypZjTAfA8uRHlFUMTwb77cenS9PPRBxuSnctC531asS9Xg3DqC5nsRxBBprgfCKotn5S8nBSD1ceHh84NASyzLSBft3xSMbg7f/2i7MQ+pGVwLncusBU6E/drnMFwZBleo+9M8Tf96axY5zuUp90MUTpSgt0= + - secure: bCDDR6+I7PmSkuTYZv1HF/z98ANX/SFEESUCqxVmV5Gs0zFC0vQXaPJQ2xaJNRop1HZBFMZLeMMPleb0iOs985smpvK2F6Rbop9Tu+Vyo0uKqv9tbZ7F8Nfgnv9suHKZlL84FNeUQZJX6vsFIYPEJ/r7K5P/M0PdUy++fEwxEhU= + - secure: ewXnzbkgCIHpDWtaWGMa1OYZJ/ki99zcIl4jcDPIC0eB3njX/WgfcC6i0Ke9mLqDqwXarWJ6helm22sNh+xtQiz6isfBtBX+novfRt9AANrBe3koCMUemMDy7oh5VflBaFNP0DVb8LSCnwf6dx6ZB5E9EB8knvk40quc/cXpGjY= + - COMMIT=${TRAVIS_COMMIT::8} diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 00000000..af94eac8 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,46 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at contact@sismicsdocs.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] + +[homepage]: http://contributor-covenant.org +[version]: http://contributor-covenant.org/version/1/4/ diff --git a/Dockerfile b/Dockerfile index 62efec59..74764c72 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,11 +1,11 @@ -FROM sismics/jetty:9.2.20-jdk7 -MAINTAINER benjamin.gam@gmail.com +FROM sismics/ubuntu-jetty:9.4.12 +MAINTAINER b.gamard@sismics.com -RUN apt-get update && apt-get -y -q install tesseract-ocr tesseract-ocr-fra tesseract-ocr-jpn && \ +RUN apt-get update && apt-get -y -q install ffmpeg mediainfo tesseract-ocr tesseract-ocr-fra tesseract-ocr-ita tesseract-ocr-kor tesseract-ocr-rus tesseract-ocr-ukr tesseract-ocr-spa tesseract-ocr-ara tesseract-ocr-hin tesseract-ocr-deu tesseract-ocr-pol tesseract-ocr-jpn tesseract-ocr-por tesseract-ocr-tha tesseract-ocr-jpn tesseract-ocr-chi-sim tesseract-ocr-chi-tra && \ apt-get clean && rm -rf /var/lib/apt/lists/* -ENV TESSDATA_PREFIX /usr/share/tesseract-ocr -ENV LC_NUMERIC C +# Remove the embedded javax.mail jar from Jetty +RUN rm -f /opt/jetty/lib/mail/javax.mail.glassfish-*.jar -ADD docs-web/target/docs-web-*.war /opt/jetty/webapps/docs.war ADD docs.xml /opt/jetty/webapps/docs.xml +ADD docs-web/target/docs-web-*.war /opt/jetty/webapps/docs.war diff --git a/README.md b/README.md index d2f9eb4f..21876f18 100644 --- a/README.md +++ b/README.md @@ -1,52 +1,89 @@ -Sismics Docs [![Build Status](https://secure.travis-ci.org/sismics/docs.png)](http://travis-ci.org/sismics/docs) -============ +

+ Sismics Docs +

-_Web interface_ +[![Twitter: @sismicsdocs](https://img.shields.io/badge/contact-@sismicsdocs-blue.svg?style=flat)](https://twitter.com/sismicsdocs) +[![License: GPL v2](https://img.shields.io/badge/License-GPL%20v2-blue.svg)](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html) +[![Build Status](https://secure.travis-ci.org/sismics/docs.png)](http://travis-ci.org/sismics/docs) -![Web interface](http://sismics.com/docs/screenshot1.png) +Docs is an open source, lightweight document management system for individuals and businesses. -_Android application_ +**Discuss it on [Product Hunt](https://www.producthunt.com/posts/sismics-docs) 🦄** -![Android documents list](http://sismics.com/docs/android1.png) ![Android navigation](http://sismics.com/docs/android2.png) ![Android document details](http://sismics.com/docs/android3.png) ![Android document actions](http://sismics.com/docs/android4.png) +
+

+ ✨ We just launched a Cloud version of Sismics Docs! Head to sismicsdocs.com for more informations ✨ +

+
-What is Docs? ---------------- +![New!](https://www.sismicsdocs.com/img/laptop-demo.png?20180301) -Docs is an open source, lightweight document management system. +Demo +---- -Docs is written in Java, and may be run on any operating system with Java support. +A demo is available at [demo.sismicsdocs.com](https://demo.sismicsdocs.com) +- Guest login is enabled with read access on all documents +- "admin" login with "admin" password +- "demo" login with "password" password Features -------- - Responsive user interface - Optical character recognition -- Support image, PDF, ODT and DOCX files -- Flexible search engine +- Support image, PDF, ODT, DOCX, PPTX files +- Video file support ![New!](https://www.sismics.com/public/img/new.png) +- Flexible search engine with suggestions and highlighting - Full text search in all supported files - All [Dublin Core](http://dublincore.org/) metadata +- Workflow system ![New!](https://www.sismics.com/public/img/new.png) - 256-bit AES encryption of stored files - Tag system with nesting +- Import document from email (EML format) ![New!](https://www.sismics.com/public/img/new.png) +- Automatic inbox scanning and importing ![New!](https://www.sismics.com/public/img/new.png) - User/group permission system +- 2-factor authentication - Hierarchical groups - Audit log - Comments - Storage quota per user - Document sharing by URL - RESTful Web API +- Webhooks to trigger external service ![New!](https://www.sismics.com/public/img/new.png) - Fully featured Android client -- Tested to 100k documents +- [Bulk files importer](https://github.com/sismics/docs/tree/master/docs-importer) (single or scan mode) ![New!](https://www.sismics.com/public/img/new.png) +- Tested to one million documents -Download --------- +Install with Docker +------------------- -The latest release is downloadable here: in WAR format. -You will need a Java webapp server to run it, like [Jetty](http://eclipse.org/jetty/) or [Tomcat](http://tomcat.apache.org/) +From a Docker host, run this command to download and install Sismics Docs. The server will run on . +**The default admin password is "admin". Don't forget to change it before going to production.** + + docker run --rm --name sismics_docs_latest -d -e DOCS_BASE_URL='http://[your-docker-host-ip]:8100' -p 8100:8080 -v sismics_docs_latest:/data sismics/docs:latest + **Note:** You will need to change [your-docker-host-ip] with the IP address or FQDN of your docker host e.g. + + FQDN: http://docs.sismics.com + IP: http://192.168.100.10 + +Manual installation +------------------- + +#### Requirements +- Java 8 with the [Java Cryptography Extension](http://www.oracle.com/technetwork/java/javase/downloads/jce-7-download-432124.html) +- Tesseract 3 or 4 for OCR +- ffmpeg for video thumbnails +- mediainfo for video metadata extraction +- A webapp server like [Jetty](http://eclipse.org/jetty/) or [Tomcat](http://tomcat.apache.org/) + +#### Download +The latest release is downloadable here: in WAR format. +**The default admin password is "admin". Don't forget to change it before going to production.** How to build Docs from the sources ---------------------------------- -Prerequisites: JDK 7 with JCE, Maven 3, Tesseract 3.02 +Prerequisites: JDK 8 with JCE, Maven 3, Tesseract 3 or 4 Docs is organized in several Maven modules: @@ -77,6 +114,24 @@ From the `docs-web` directory: You will get your deployable WAR in the `docs-web/target` directory. +Contributing +------------ + +All contributions are more than welcomed. Contributions may close an issue, fix a bug (reported or not reported), improve the existing code, add new feature, and so on. + +The `master` branch is the default and base branch for the project. It is used for development and all Pull Requests should go there. + + +Community +--------- + +Get updates on Sismics Docs' development and chat with the project maintainers: + +- Follow [@sismicsdocs on Twitter](https://twitter.com/sismicsdocs) +- Read and subscribe to [The Official Sismics Docs Blog](https://blog.sismicsdocs.com/) +- Check the [Official Website](https://www.sismicsdocs.com) +- Join us [on Facebook](https://www.facebook.com/sismicsdocs) + License ------- diff --git a/docs-android/app/build.gradle b/docs-android/app/build.gradle index 6c2f2d82..25cfc4f4 100644 --- a/docs-android/app/build.gradle +++ b/docs-android/app/build.gradle @@ -1,57 +1,43 @@ buildscript { repositories { jcenter() + google() } dependencies { - classpath 'com.android.tools.build:gradle:2.2.3' + classpath 'com.android.tools.build:gradle:3.2.1' } } apply plugin: 'com.android.application' repositories { jcenter() + google() } android { - compileSdkVersion 24 - buildToolsVersion '24' + compileSdkVersion 28 defaultConfig { minSdkVersion 14 - targetSdkVersion 24 + targetSdkVersion 28 versionCode 1 versionName '1.0' } - signingConfigs { - release { - storeFile file(System.getenv('TRACKINO_STORE_PATH')) - storePassword System.getenv('TRACKINO_STORE_PASS') - keyAlias System.getenv('TRACKINO_STORE_ALIAS') - keyPassword System.getenv('TRACKINO_STORE_KEYPASS') - } - } - - buildTypes { - release { - signingConfig signingConfigs.release - } - } - lintOptions { abortOnError false } } dependencies { - compile fileTree(dir: 'libs', include: '*.jar') - compile 'com.android.support:appcompat-v7:24.0.0' - compile 'com.android.support:recyclerview-v7:24.0.0' - compile 'com.android.support:design:24.0.0' - compile 'it.sephiroth.android.library.imagezoom:imagezoom:1.0.5' - compile 'org.greenrobot:eventbus:3.0.0' - compile 'com.squareup.picasso:picasso:2.5.2' - compile 'com.squareup.okhttp3:okhttp:3.4.0' - compile 'com.squareup.okhttp3:okhttp-urlconnection:3.4.0' - compile 'com.jakewharton.picasso:picasso2-okhttp3-downloader:1.0.2' + implementation fileTree(dir: 'libs', include: '*.jar') + implementation 'com.android.support:appcompat-v7:28.0.0' + implementation 'com.android.support:recyclerview-v7:28.0.0' + implementation 'com.android.support:design:28.0.0' + implementation 'it.sephiroth.android.library.imagezoom:imagezoom:1.0.5' + implementation 'org.greenrobot:eventbus:3.1.1' + implementation 'com.squareup.picasso:picasso:2.5.2' + implementation 'com.squareup.okhttp3:okhttp:3.10.0' + implementation 'com.squareup.okhttp3:okhttp-urlconnection:3.10.0' + implementation 'com.jakewharton.picasso:picasso2-okhttp3-downloader:1.1.0' } diff --git a/docs-android/app/src/main/AndroidManifest.xml b/docs-android/app/src/main/AndroidManifest.xml index b56057b5..66708a1d 100644 --- a/docs-android/app/src/main/AndroidManifest.xml +++ b/docs-android/app/src/main/AndroidManifest.xml @@ -28,6 +28,7 @@ android:name=".activity.MainActivity" android:label="@string/app_name" android:launchMode="singleTop" + android:theme="@style/AppTheme.NoActionBar" android:windowSoftInputMode="adjustNothing"> @@ -43,6 +44,9 @@ + + + { + private int toolbarHeight; + + public ScrollingFABBehavior(Context context, AttributeSet attrs) { + super(context, attrs); + this.toolbarHeight = getToolbarHeight(context); + } + + @Override + public boolean layoutDependsOn(@NonNull CoordinatorLayout parent, @NonNull FloatingActionButton fab, @NonNull View dependency) { + return dependency instanceof AppBarLayout; + } + + @Override + public boolean onDependentViewChanged(@NonNull CoordinatorLayout parent, @NonNull FloatingActionButton fab, @NonNull View dependency) { + if (dependency instanceof AppBarLayout) { + CoordinatorLayout.LayoutParams lp = (CoordinatorLayout.LayoutParams) fab.getLayoutParams(); + int fabBottomMargin = lp.bottomMargin; + int distanceToScroll = fab.getHeight() + fabBottomMargin; + float ratio = dependency.getY() /(float) toolbarHeight; + fab.setTranslationY(- distanceToScroll * ratio); + } + return true; + } + + private int getToolbarHeight(Context context) { + final TypedArray styledAttributes = context.getTheme().obtainStyledAttributes( + new int[] { R.attr.actionBarSize }); + int toolbarHeight = (int) styledAttributes.getDimension(0, 0); + styledAttributes.recycle(); + + return toolbarHeight; + } +} \ No newline at end of file diff --git a/docs-android/app/src/main/res/drawable-xhdpi/jpn.png b/docs-android/app/src/main/res/drawable-xhdpi/jpn.png deleted file mode 100644 index b712c94d..00000000 Binary files a/docs-android/app/src/main/res/drawable-xhdpi/jpn.png and /dev/null differ diff --git a/docs-android/app/src/main/res/drawable-xxhdpi/jpn.png b/docs-android/app/src/main/res/drawable-xxhdpi/jpn.png deleted file mode 100644 index 4846fda0..00000000 Binary files a/docs-android/app/src/main/res/drawable-xxhdpi/jpn.png and /dev/null differ diff --git a/docs-android/app/src/main/res/layout/doc_list_fragment.xml b/docs-android/app/src/main/res/layout/doc_list_fragment.xml index ff83398e..979a1672 100644 --- a/docs-android/app/src/main/res/layout/doc_list_fragment.xml +++ b/docs-android/app/src/main/res/layout/doc_list_fragment.xml @@ -1,6 +1,5 @@ @@ -37,17 +36,4 @@ android:textSize="16sp" android:layout_centerInParent="true"/> - - \ No newline at end of file diff --git a/docs-android/app/src/main/res/layout/main_activity.xml b/docs-android/app/src/main/res/layout/main_activity.xml index cc9d2fdc..e5375a54 100644 --- a/docs-android/app/src/main/res/layout/main_activity.xml +++ b/docs-android/app/src/main/res/layout/main_activity.xml @@ -6,11 +6,47 @@ android:layout_width="match_parent" android:layout_height="match_parent"> - + android:layout_height="match_parent"> + + + + + + + + + + + + + + + + + Email invalide + Trop court (min. %d) + Trop long (max. %d) + Requis + Seuls les lettres et les nombres sont autorisés + + + Ouvrir le menu de navigation + Fermer le menu de navigation + github.com/sismics/docs et entrer son URL ci-dessous]]> + Serveur + Nom d\'utilisateur + Mot de passe + Connexion + OK + Annuler + Echec de la connexion + Mauvais nom d\'utilisateur ou de mot de passe + Erreur réseau + Erreur réseau, veuillez vérifier votre connexion internet et l\'URL du serveur + URL invalide + Veuillez vérifier l\URL du serveur et réessayer + Une erreur s\'est produite, un rapport a été envoyé afin de corriger ce problème + Date création + Télécharger ce fichier + Télécharger + Rechercher dans les documents + Tous les documents + Documents partagés + Tous les tags + Aucun tag + Erreur de chargement des tags + Aucun document + Erreur de chargement des documents + Aucun fichier + Erreur de chargement des fichiers + Nouveau document + Partage + Fermer + Ajouter + Nom du partage (facultatif) + Ce document n\'est pas partagé + Supprimer ce partage + Envoi ce lien de partage + Erreur de chargement des partages + Erreur lors de l\'ajout du partage + Lien de partage + Erreur lors de la suppression de ce partage + Envoi ce lien de partage à + Aj. fichier + Envoyer un fichier depuis + Paramètres + Déconnexion + Version + Build + Paramètres avancés + A propors + GitHub + Rapporter un bug + Vider le cache + Nettoyer les fichiers en cache + Cache vidé + Vider l\'historique de recherche + Supprimer les recherches récentes + Historique de recherche vidé + Taille du cache + Enregistrer + Modifier + Erreur réseau, veuillez réessayer + Veuillez patienter + Envoi des données + Suppr. + Supprimer le document + Etes-vous sûr de vouloir supprimer ce document et tous les fichiers associés ? + Erreur réseau lors de la suppression de ce document + Suppression du document + Supprimer le fichier + Etes-vous sûr de vouloir supprimer ce fichier ? + Erreur réseau lors de la suppression du fichier + Suppression du fichier + Erreur lors de la lecture du fichier + Sismics Docs + Envoi du nouveau fichier + Erreur lors de l\'envoi du nouveau fichier + Supprimer ce fichier + Recherche avancée + Rechercher + Ajouter des tags + Date de création + Description + Titre + Recherche simple + Recherche texte intégral + Créateur + Après cette date + Avant cette date + Rechercher dans les tags + Toutes les langues + Afficher/masquer les informations + Qui a accès + Commentaires + Aucun commentaire + Erreur de chargement des commentaires + Envoyer + Ajouter un commentaire + Erreur lors de l\'ajout du commentaire + Ajout du commentaire + Supprimer le commentaire + Suppression du commentaire + Erreur lors de la suppression du commentaire + PDF + Télécharger + Marge + Ajuster les images à la page + Exporter les commentaires + Exporter les métadonnées + mm + Export de fichier Sismics Docs + Export de document Sismics Docs + Export PDF Sismics Docs + Activité récente + Activité + E-mail + Quota de stockage + %1$d/%2$d Mo + Code de validation + Partagé + Langue + Couverture + Type + Source + Format + Editeur + Identifiant + Sujet + Droits + Contributeurs + Relations + + diff --git a/docs-android/app/src/main/res/values/strings.xml b/docs-android/app/src/main/res/values/strings.xml index 87e56123..5d453293 100644 --- a/docs-android/app/src/main/res/values/strings.xml +++ b/docs-android/app/src/main/res/values/strings.xml @@ -9,10 +9,10 @@ Only letters and numbers - Sismics Docs + Sismics Docs Open navigation drawer Close navigation drawer - www.sismics.com/docs and enter your server URL below]]> + github.com/sismics/docs and enter its below]]> Server Username Password @@ -46,7 +46,7 @@ Name the share (optional) This document is not currently shared Delete this share - Send this share + Send this share link Error loading shares Error adding share Share link @@ -69,9 +69,8 @@ Wipe the recent search suggestions Search history cleared Cache size - French - English - Japanese + Français + English Save Edit Network error, please try again diff --git a/docs-android/app/src/main/res/values/styles.xml b/docs-android/app/src/main/res/values/styles.xml index ca5eb5c9..953c239b 100644 --- a/docs-android/app/src/main/res/values/styles.xml +++ b/docs-android/app/src/main/res/values/styles.xml @@ -6,6 +6,14 @@ @color/colorAccent + + +
userType: Required!
- userType = {{userType}}
- myForm.input.$valid = {{myForm.input.$valid}}
- myForm.input.$error = {{myForm.input.$error}}
- myForm.$valid = {{myForm.$valid}}
- myForm.$error.required = {{!!myForm.$error.required}}
+ userType = {{userType}}
+ myForm.input.$valid = {{myForm.input.$valid}}
+ myForm.input.$error = {{myForm.input.$error}}
+ myForm.$valid = {{myForm.$valid}}
+ myForm.$error.required = {{!!myForm.$error.required}}
- - + + it('should initialize to model', function() { - expect(binding('userType')).toEqual('guest'); - expect(binding('myForm.input.$valid')).toEqual('true'); + var userType = element(by.binding('userType')); + var valid = element(by.binding('myForm.input.$valid')); + + expect(userType.getText()).toContain('guest'); + expect(valid.getText()).toContain('true'); }); it('should be invalid if empty', function() { - input('userType').enter(''); - expect(binding('userType')).toEqual(''); - expect(binding('myForm.input.$valid')).toEqual('false'); + var userType = element(by.binding('userType')); + var valid = element(by.binding('myForm.input.$valid')); + var userInput = element(by.model('userType')); + + userInput.clear(); + userInput.sendKeys(''); + + expect(userType.getText()).toEqual('userType ='); + expect(valid.getText()).toContain('false'); }); - - + + + * + * @param {string=} name Name of the form. If specified, the form controller will be published into + * related scope, under this name. */ var formDirectiveFactory = function(isNgForm) { - return ['$timeout', function($timeout) { + return ['$timeout', '$parse', function($timeout, $parse) { var formDirective = { name: 'form', restrict: isNgForm ? 'EAC' : 'E', + require: ['form', '^^?form'], //first is the form's own ctrl, second is an optional parent form controller: FormController, - compile: function() { + compile: function ngFormCompile(formElement, attr) { + // Setup initial state of the control + formElement.addClass(PRISTINE_CLASS).addClass(VALID_CLASS); + + var nameAttr = attr.name ? 'name' : (isNgForm && attr.ngForm ? 'ngForm' : false); + return { - pre: function(scope, formElement, attr, controller) { - if (!attr.action) { + pre: function ngFormPreLink(scope, formElement, attr, ctrls) { + var controller = ctrls[0]; + + // if `action` attr is not present on the form, prevent the default action (submission) + if (!('action' in attr)) { // we can't use jq events because if a form is destroyed during submission the default // action is not prevented. see #1238 // // IE 9 is not affected because it doesn't fire a submit event and try to do a full // page reload if the form was destroyed by submission of the form via a click handler // on a button in the form. Looks like an IE9 specific bug. - var preventDefaultListener = function(event) { - event.preventDefault - ? event.preventDefault() - : event.returnValue = false; // IE + var handleFormSubmission = function(event) { + scope.$apply(function() { + controller.$commitViewValue(); + controller.$setSubmitted(); + }); + + event.preventDefault(); }; - addEventListenerFn(formElement[0], 'submit', preventDefaultListener); + formElement[0].addEventListener('submit', handleFormSubmission); // unregister the preventDefault listener so that we don't not leak memory but in a // way that will achieve the prevention of the default action. formElement.on('$destroy', function() { $timeout(function() { - removeEventListenerFn(formElement[0], 'submit', preventDefaultListener); + formElement[0].removeEventListener('submit', handleFormSubmission); }, 0, false); }); } - var parentFormCtrl = formElement.parent().controller('form'), - alias = attr.name || attr.ngForm; + var parentFormCtrl = ctrls[1] || controller.$$parentForm; + parentFormCtrl.$addControl(controller); - if (alias) { - setter(scope, alias, controller, alias); - } - if (parentFormCtrl) { - formElement.on('$destroy', function() { - parentFormCtrl.$removeControl(controller); - if (alias) { - setter(scope, alias, undefined, alias); - } - extend(controller, nullFormCtrl); //stop propagating child destruction handlers upwards + var setter = nameAttr ? getSetter(controller.$name) : noop; + + if (nameAttr) { + setter(scope, controller); + attr.$observe(nameAttr, function(newValue) { + if (controller.$name === newValue) return; + setter(scope, undefined); + controller.$$parentForm.$$renameControl(controller, newValue); + setter = getSetter(controller.$name); + setter(scope, controller); }); } + formElement.on('$destroy', function() { + controller.$$parentForm.$removeControl(controller); + setter(scope, undefined); + extend(controller, nullFormCtrl); //stop propagating child destruction handlers upwards + }); } }; } }; return formDirective; + + function getSetter(expression) { + if (expression === '') { + //create an assignable expression, so forms with an empty name can be renamed later + return $parse('this[""]').assign; + } + return $parse(expression).assign || noop; + } }]; }; var formDirective = formDirectiveFactory(); var ngFormDirective = formDirectiveFactory(true); + + +// helper methods + function setupValidity(instance) { + instance.$$classCache = {}; + instance.$$classCache[INVALID_CLASS] = !(instance.$$classCache[VALID_CLASS] = instance.$$element.hasClass(VALID_CLASS)); + } + function addSetValidityMethod(context) { + var clazz = context.clazz, + set = context.set, + unset = context.unset; + + clazz.prototype.$setValidity = function(validationErrorKey, state, controller) { + if (isUndefined(state)) { + createAndSet(this, '$pending', validationErrorKey, controller); + } else { + unsetAndCleanup(this, '$pending', validationErrorKey, controller); + } + if (!isBoolean(state)) { + unset(this.$error, validationErrorKey, controller); + unset(this.$$success, validationErrorKey, controller); + } else { + if (state) { + unset(this.$error, validationErrorKey, controller); + set(this.$$success, validationErrorKey, controller); + } else { + set(this.$error, validationErrorKey, controller); + unset(this.$$success, validationErrorKey, controller); + } + } + if (this.$pending) { + cachedToggleClass(this, PENDING_CLASS, true); + this.$valid = this.$invalid = undefined; + toggleValidationCss(this, '', null); + } else { + cachedToggleClass(this, PENDING_CLASS, false); + this.$valid = isObjectEmpty(this.$error); + this.$invalid = !this.$valid; + toggleValidationCss(this, '', this.$valid); + } + + // re-read the state as the set/unset methods could have + // combined state in this.$error[validationError] (used for forms), + // where setting/unsetting only increments/decrements the value, + // and does not replace it. + var combinedState; + if (this.$pending && this.$pending[validationErrorKey]) { + combinedState = undefined; + } else if (this.$error[validationErrorKey]) { + combinedState = false; + } else if (this.$$success[validationErrorKey]) { + combinedState = true; + } else { + combinedState = null; + } + + toggleValidationCss(this, validationErrorKey, combinedState); + this.$$parentForm.$setValidity(validationErrorKey, combinedState, this); + }; + + function createAndSet(ctrl, name, value, controller) { + if (!ctrl[name]) { + ctrl[name] = {}; + } + set(ctrl[name], value, controller); + } + + function unsetAndCleanup(ctrl, name, value, controller) { + if (ctrl[name]) { + unset(ctrl[name], value, controller); + } + if (isObjectEmpty(ctrl[name])) { + ctrl[name] = undefined; + } + } + + function cachedToggleClass(ctrl, className, switchValue) { + if (switchValue && !ctrl.$$classCache[className]) { + ctrl.$$animate.addClass(ctrl.$$element, className); + ctrl.$$classCache[className] = true; + } else if (!switchValue && ctrl.$$classCache[className]) { + ctrl.$$animate.removeClass(ctrl.$$element, className); + ctrl.$$classCache[className] = false; + } + } + + function toggleValidationCss(ctrl, validationErrorKey, isValid) { + validationErrorKey = validationErrorKey ? '-' + snake_case(validationErrorKey, '-') : ''; + + cachedToggleClass(ctrl, VALID_CLASS + validationErrorKey, isValid === true); + cachedToggleClass(ctrl, INVALID_CLASS + validationErrorKey, isValid === false); + } + } + + function isObjectEmpty(obj) { + if (obj) { + for (var prop in obj) { + if (obj.hasOwnProperty(prop)) { + return false; + } + } + } + return true; + } + /* global + VALID_CLASS: false, + INVALID_CLASS: false, + PRISTINE_CLASS: false, + DIRTY_CLASS: false, + ngModelMinErr: false +*/ - -VALID_CLASS, - -INVALID_CLASS, - -PRISTINE_CLASS, - -DIRTY_CLASS - */ +// Regex code was initially obtained from SO prior to modification: https://stackoverflow.com/questions/3143070/javascript-regex-iso-datetime#answer-3143231 + var ISO_DATE_REGEXP = /^\d{4,}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d\.\d+(?:[+-][0-2]\d:[0-5]\d|Z)$/; +// See valid URLs in RFC3987 (http://tools.ietf.org/html/rfc3987) +// Note: We are being more lenient, because browsers are too. +// 1. Scheme +// 2. Slashes +// 3. Username +// 4. Password +// 5. Hostname +// 6. Port +// 7. Path +// 8. Query +// 9. Fragment +// 1111111111111111 222 333333 44444 55555555555555555555555 666 77777777 8888888 999 + var URL_REGEXP = /^[a-z][a-z\d.+-]*:\/*(?:[^:@]+(?::[^@]+)?@)?(?:[^\s:/?#]+|\[[a-f\d:]+])(?::\d+)?(?:\/[^?#]*)?(?:\?[^#]*)?(?:#.*)?$/i; +// eslint-disable-next-line max-len + var EMAIL_REGEXP = /^(?=.{1,254}$)(?=.{1,64}@)[-!#$%&'*+/0-9=?A-Z^_`a-z{|}~]+(\.[-!#$%&'*+/0-9=?A-Z^_`a-z{|}~]+)*@[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?(\.[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?)*$/; + var NUMBER_REGEXP = /^\s*(-|\+)?(\d+|(\d*(\.\d*)))([eE][+-]?\d+)?\s*$/; + var DATE_REGEXP = /^(\d{4,})-(\d{2})-(\d{2})$/; + var DATETIMELOCAL_REGEXP = /^(\d{4,})-(\d\d)-(\d\d)T(\d\d):(\d\d)(?::(\d\d)(\.\d{1,3})?)?$/; + var WEEK_REGEXP = /^(\d{4,})-W(\d\d)$/; + var MONTH_REGEXP = /^(\d{4,})-(\d\d)$/; + var TIME_REGEXP = /^(\d\d):(\d\d)(?::(\d\d)(\.\d{1,3})?)?$/; - var URL_REGEXP = /^(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?$/; - var EMAIL_REGEXP = /^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,6}$/; - var NUMBER_REGEXP = /^\s*(\-|\+)?(\d+|(\d*(\.\d*)))\s*$/; + var PARTIAL_VALIDATION_EVENTS = 'keydown wheel mousedown'; + var PARTIAL_VALIDATION_TYPES = createMap(); + forEach('date,datetime-local,month,time,week'.split(','), function(type) { + PARTIAL_VALIDATION_TYPES[type] = true; + }); var inputType = { /** - * @ngdoc inputType - * @name ng.directive:input.text + * @ngdoc input + * @name input[text] * * @description - * Standard HTML text input with angular data binding. + * Standard HTML text input with angular data binding, inherited by most of the `input` elements. + * * * @param {string} ngModel Assignable angular expression to data-bind to. * @param {string=} name Property name of the form under which the control is published. @@ -15642,78 +23914,639 @@ * @param {number=} ngMinlength Sets `minlength` validation error key if the value is shorter than * minlength. * @param {number=} ngMaxlength Sets `maxlength` validation error key if the value is longer than - * maxlength. - * @param {string=} ngPattern Sets `pattern` validation error key if the value does not match the - * RegExp pattern expression. Expected value is `/regexp/` for inline patterns or `regexp` for - * patterns defined as scope expressions. + * maxlength. Setting the attribute to a negative or non-numeric value, allows view values of + * any length. + * @param {string=} pattern Similar to `ngPattern` except that the attribute value is the actual string + * that contains the regular expression body that will be converted to a regular expression + * as in the ngPattern directive. + * @param {string=} ngPattern Sets `pattern` validation error key if the ngModel {@link ngModel.NgModelController#$viewValue $viewValue} + * does not match a RegExp found by evaluating the Angular expression given in the attribute value. + * If the expression evaluates to a RegExp object, then this is used directly. + * If the expression evaluates to a string, then it will be converted to a RegExp + * after wrapping it in `^` and `$` characters. For instance, `"abc"` will be converted to + * `new RegExp('^abc$')`.
+ * **Note:** Avoid using the `g` flag on the RegExp, as it will cause each successive search to + * start at the index of the last search's match, thus not taking the whole input value into + * account. * @param {string=} ngChange Angular expression to be executed when input changes due to user * interaction with the input element. * @param {boolean=} [ngTrim=true] If set to false Angular will not automatically trim the input. + * This parameter is ignored for input[type=password] controls, which will never trim the + * input. * * @example - - + + -
- Single word: + + +
Required! Single word only! +
+ text = {{example.text}}
+ myForm.input.$valid = {{myForm.input.$valid}}
+ myForm.input.$error = {{myForm.input.$error}}
+ myForm.$valid = {{myForm.$valid}}
+ myForm.$error.required = {{!!myForm.$error.required}}
+
+
+ + var text = element(by.binding('example.text')); + var valid = element(by.binding('myForm.input.$valid')); + var input = element(by.model('example.text')); - text = {{text}}
+ it('should initialize to model', function() { + expect(text.getText()).toContain('guest'); + expect(valid.getText()).toContain('true'); + }); + + it('should be invalid if empty', function() { + input.clear(); + input.sendKeys(''); + + expect(text.getText()).toEqual('text ='); + expect(valid.getText()).toContain('false'); + }); + + it('should be invalid if multi word', function() { + input.clear(); + input.sendKeys('hello world'); + + expect(valid.getText()).toContain('false'); + }); +
+
+ */ + 'text': textInputType, + + /** + * @ngdoc input + * @name input[date] + * + * @description + * Input with date validation and transformation. In browsers that do not yet support + * the HTML5 date input, a text element will be used. In that case, text must be entered in a valid ISO-8601 + * date format (yyyy-MM-dd), for example: `2009-01-06`. Since many + * modern browsers do not yet support this input type, it is important to provide cues to users on the + * expected input format via a placeholder or label. + * + * The model must always be a Date object, otherwise Angular will throw an error. + * Invalid `Date` objects (dates whose `getTime()` is `NaN`) will be rendered as an empty string. + * + * The timezone to be used to read/write the `Date` instance in the model can be defined using + * {@link ng.directive:ngModelOptions ngModelOptions}. By default, this is the timezone of the browser. + * + * @param {string} ngModel Assignable angular expression to data-bind to. + * @param {string=} name Property name of the form under which the control is published. + * @param {string=} min Sets the `min` validation error key if the value entered is less than `min`. This must be a + * valid ISO date string (yyyy-MM-dd). You can also use interpolation inside this attribute + * (e.g. `min="{{minDate | date:'yyyy-MM-dd'}}"`). Note that `min` will also add native HTML5 + * constraint validation. + * @param {string=} max Sets the `max` validation error key if the value entered is greater than `max`. This must be + * a valid ISO date string (yyyy-MM-dd). You can also use interpolation inside this attribute + * (e.g. `max="{{maxDate | date:'yyyy-MM-dd'}}"`). Note that `max` will also add native HTML5 + * constraint validation. + * @param {(date|string)=} ngMin Sets the `min` validation constraint to the Date / ISO date string + * the `ngMin` expression evaluates to. Note that it does not set the `min` attribute. + * @param {(date|string)=} ngMax Sets the `max` validation constraint to the Date / ISO date string + * the `ngMax` expression evaluates to. Note that it does not set the `max` attribute. + * @param {string=} required Sets `required` validation error key if the value is not entered. + * @param {string=} ngRequired Adds `required` attribute and `required` validation constraint to + * the element when the ngRequired expression evaluates to true. Use `ngRequired` instead of + * `required` when you want to data-bind to the `required` attribute. + * @param {string=} ngChange Angular expression to be executed when input changes due to user + * interaction with the input element. + * + * @example + + + +
+ + +
+ + Required! + + Not a valid date! +
+ value = {{example.value | date: "yyyy-MM-dd"}}
myForm.input.$valid = {{myForm.input.$valid}}
myForm.input.$error = {{myForm.input.$error}}
myForm.$valid = {{myForm.$valid}}
myForm.$error.required = {{!!myForm.$error.required}}
-
- + + + var value = element(by.binding('example.value | date: "yyyy-MM-dd"')); + var valid = element(by.binding('myForm.input.$valid')); + + // currently protractor/webdriver does not support + // sending keys to all known HTML5 input controls + // for various browsers (see https://github.com/angular/protractor/issues/562). + function setInput(val) { + // set the value of the element and force validation. + var scr = "var ipt = document.getElementById('exampleInput'); " + + "ipt.value = '" + val + "';" + + "angular.element(ipt).scope().$apply(function(s) { s.myForm[ipt.name].$setViewValue('" + val + "'); });"; + browser.executeScript(scr); + } + it('should initialize to model', function() { - expect(binding('text')).toEqual('guest'); - expect(binding('myForm.input.$valid')).toEqual('true'); - }); + expect(value.getText()).toContain('2013-10-22'); + expect(valid.getText()).toContain('myForm.input.$valid = true'); + }); it('should be invalid if empty', function() { - input('text').enter(''); - expect(binding('text')).toEqual(''); - expect(binding('myForm.input.$valid')).toEqual('false'); - }); + setInput(''); + expect(value.getText()).toEqual('value ='); + expect(valid.getText()).toContain('myForm.input.$valid = false'); + }); - it('should be invalid if multi word', function() { - input('text').enter('hello world'); - expect(binding('myForm.input.$valid')).toEqual('false'); - }); - - it('should not be trimmed', function() { - input('text').enter('untrimmed '); - expect(binding('text')).toEqual('untrimmed '); - expect(binding('myForm.input.$valid')).toEqual('true'); - }); - -
+ it('should be invalid if over max', function() { + setInput('2015-01-01'); + expect(value.getText()).toContain(''); + expect(valid.getText()).toContain('myForm.input.$valid = false'); + }); + + */ - 'text': textInputType, - + 'date': createDateInputType('date', DATE_REGEXP, + createDateParser(DATE_REGEXP, ['yyyy', 'MM', 'dd']), + 'yyyy-MM-dd'), /** - * @ngdoc inputType - * @name ng.directive:input.number + * @ngdoc input + * @name input[datetime-local] + * + * @description + * Input with datetime validation and transformation. In browsers that do not yet support + * the HTML5 date input, a text element will be used. In that case, the text must be entered in a valid ISO-8601 + * local datetime format (yyyy-MM-ddTHH:mm:ss), for example: `2010-12-28T14:57:00`. + * + * The model must always be a Date object, otherwise Angular will throw an error. + * Invalid `Date` objects (dates whose `getTime()` is `NaN`) will be rendered as an empty string. + * + * The timezone to be used to read/write the `Date` instance in the model can be defined using + * {@link ng.directive:ngModelOptions ngModelOptions}. By default, this is the timezone of the browser. + * + * @param {string} ngModel Assignable angular expression to data-bind to. + * @param {string=} name Property name of the form under which the control is published. + * @param {string=} min Sets the `min` validation error key if the value entered is less than `min`. + * This must be a valid ISO datetime format (yyyy-MM-ddTHH:mm:ss). You can also use interpolation + * inside this attribute (e.g. `min="{{minDatetimeLocal | date:'yyyy-MM-ddTHH:mm:ss'}}"`). + * Note that `min` will also add native HTML5 constraint validation. + * @param {string=} max Sets the `max` validation error key if the value entered is greater than `max`. + * This must be a valid ISO datetime format (yyyy-MM-ddTHH:mm:ss). You can also use interpolation + * inside this attribute (e.g. `max="{{maxDatetimeLocal | date:'yyyy-MM-ddTHH:mm:ss'}}"`). + * Note that `max` will also add native HTML5 constraint validation. + * @param {(date|string)=} ngMin Sets the `min` validation error key to the Date / ISO datetime string + * the `ngMin` expression evaluates to. Note that it does not set the `min` attribute. + * @param {(date|string)=} ngMax Sets the `max` validation error key to the Date / ISO datetime string + * the `ngMax` expression evaluates to. Note that it does not set the `max` attribute. + * @param {string=} required Sets `required` validation error key if the value is not entered. + * @param {string=} ngRequired Adds `required` attribute and `required` validation constraint to + * the element when the ngRequired expression evaluates to true. Use `ngRequired` instead of + * `required` when you want to data-bind to the `required` attribute. + * @param {string=} ngChange Angular expression to be executed when input changes due to user + * interaction with the input element. + * + * @example + + + +
+ + +
+ + Required! + + Not a valid date! +
+ value = {{example.value | date: "yyyy-MM-ddTHH:mm:ss"}}
+ myForm.input.$valid = {{myForm.input.$valid}}
+ myForm.input.$error = {{myForm.input.$error}}
+ myForm.$valid = {{myForm.$valid}}
+ myForm.$error.required = {{!!myForm.$error.required}}
+
+
+ + var value = element(by.binding('example.value | date: "yyyy-MM-ddTHH:mm:ss"')); + var valid = element(by.binding('myForm.input.$valid')); + + // currently protractor/webdriver does not support + // sending keys to all known HTML5 input controls + // for various browsers (https://github.com/angular/protractor/issues/562). + function setInput(val) { + // set the value of the element and force validation. + var scr = "var ipt = document.getElementById('exampleInput'); " + + "ipt.value = '" + val + "';" + + "angular.element(ipt).scope().$apply(function(s) { s.myForm[ipt.name].$setViewValue('" + val + "'); });"; + browser.executeScript(scr); + } + + it('should initialize to model', function() { + expect(value.getText()).toContain('2010-12-28T14:57:00'); + expect(valid.getText()).toContain('myForm.input.$valid = true'); + }); + + it('should be invalid if empty', function() { + setInput(''); + expect(value.getText()).toEqual('value ='); + expect(valid.getText()).toContain('myForm.input.$valid = false'); + }); + + it('should be invalid if over max', function() { + setInput('2015-01-01T23:59:00'); + expect(value.getText()).toContain(''); + expect(valid.getText()).toContain('myForm.input.$valid = false'); + }); + +
+ */ + 'datetime-local': createDateInputType('datetimelocal', DATETIMELOCAL_REGEXP, + createDateParser(DATETIMELOCAL_REGEXP, ['yyyy', 'MM', 'dd', 'HH', 'mm', 'ss', 'sss']), + 'yyyy-MM-ddTHH:mm:ss.sss'), + + /** + * @ngdoc input + * @name input[time] + * + * @description + * Input with time validation and transformation. In browsers that do not yet support + * the HTML5 time input, a text element will be used. In that case, the text must be entered in a valid ISO-8601 + * local time format (HH:mm:ss), for example: `14:57:00`. Model must be a Date object. This binding will always output a + * Date object to the model of January 1, 1970, or local date `new Date(1970, 0, 1, HH, mm, ss)`. + * + * The model must always be a Date object, otherwise Angular will throw an error. + * Invalid `Date` objects (dates whose `getTime()` is `NaN`) will be rendered as an empty string. + * + * The timezone to be used to read/write the `Date` instance in the model can be defined using + * {@link ng.directive:ngModelOptions ngModelOptions}. By default, this is the timezone of the browser. + * + * @param {string} ngModel Assignable angular expression to data-bind to. + * @param {string=} name Property name of the form under which the control is published. + * @param {string=} min Sets the `min` validation error key if the value entered is less than `min`. + * This must be a valid ISO time format (HH:mm:ss). You can also use interpolation inside this + * attribute (e.g. `min="{{minTime | date:'HH:mm:ss'}}"`). Note that `min` will also add + * native HTML5 constraint validation. + * @param {string=} max Sets the `max` validation error key if the value entered is greater than `max`. + * This must be a valid ISO time format (HH:mm:ss). You can also use interpolation inside this + * attribute (e.g. `max="{{maxTime | date:'HH:mm:ss'}}"`). Note that `max` will also add + * native HTML5 constraint validation. + * @param {(date|string)=} ngMin Sets the `min` validation constraint to the Date / ISO time string the + * `ngMin` expression evaluates to. Note that it does not set the `min` attribute. + * @param {(date|string)=} ngMax Sets the `max` validation constraint to the Date / ISO time string the + * `ngMax` expression evaluates to. Note that it does not set the `max` attribute. + * @param {string=} required Sets `required` validation error key if the value is not entered. + * @param {string=} ngRequired Adds `required` attribute and `required` validation constraint to + * the element when the ngRequired expression evaluates to true. Use `ngRequired` instead of + * `required` when you want to data-bind to the `required` attribute. + * @param {string=} ngChange Angular expression to be executed when input changes due to user + * interaction with the input element. + * + * @example + + + +
+ + +
+ + Required! + + Not a valid date! +
+ value = {{example.value | date: "HH:mm:ss"}}
+ myForm.input.$valid = {{myForm.input.$valid}}
+ myForm.input.$error = {{myForm.input.$error}}
+ myForm.$valid = {{myForm.$valid}}
+ myForm.$error.required = {{!!myForm.$error.required}}
+
+
+ + var value = element(by.binding('example.value | date: "HH:mm:ss"')); + var valid = element(by.binding('myForm.input.$valid')); + + // currently protractor/webdriver does not support + // sending keys to all known HTML5 input controls + // for various browsers (https://github.com/angular/protractor/issues/562). + function setInput(val) { + // set the value of the element and force validation. + var scr = "var ipt = document.getElementById('exampleInput'); " + + "ipt.value = '" + val + "';" + + "angular.element(ipt).scope().$apply(function(s) { s.myForm[ipt.name].$setViewValue('" + val + "'); });"; + browser.executeScript(scr); + } + + it('should initialize to model', function() { + expect(value.getText()).toContain('14:57:00'); + expect(valid.getText()).toContain('myForm.input.$valid = true'); + }); + + it('should be invalid if empty', function() { + setInput(''); + expect(value.getText()).toEqual('value ='); + expect(valid.getText()).toContain('myForm.input.$valid = false'); + }); + + it('should be invalid if over max', function() { + setInput('23:59:00'); + expect(value.getText()).toContain(''); + expect(valid.getText()).toContain('myForm.input.$valid = false'); + }); + +
+ */ + 'time': createDateInputType('time', TIME_REGEXP, + createDateParser(TIME_REGEXP, ['HH', 'mm', 'ss', 'sss']), + 'HH:mm:ss.sss'), + + /** + * @ngdoc input + * @name input[week] + * + * @description + * Input with week-of-the-year validation and transformation to Date. In browsers that do not yet support + * the HTML5 week input, a text element will be used. In that case, the text must be entered in a valid ISO-8601 + * week format (yyyy-W##), for example: `2013-W02`. + * + * The model must always be a Date object, otherwise Angular will throw an error. + * Invalid `Date` objects (dates whose `getTime()` is `NaN`) will be rendered as an empty string. + * + * The timezone to be used to read/write the `Date` instance in the model can be defined using + * {@link ng.directive:ngModelOptions ngModelOptions}. By default, this is the timezone of the browser. + * + * @param {string} ngModel Assignable angular expression to data-bind to. + * @param {string=} name Property name of the form under which the control is published. + * @param {string=} min Sets the `min` validation error key if the value entered is less than `min`. + * This must be a valid ISO week format (yyyy-W##). You can also use interpolation inside this + * attribute (e.g. `min="{{minWeek | date:'yyyy-Www'}}"`). Note that `min` will also add + * native HTML5 constraint validation. + * @param {string=} max Sets the `max` validation error key if the value entered is greater than `max`. + * This must be a valid ISO week format (yyyy-W##). You can also use interpolation inside this + * attribute (e.g. `max="{{maxWeek | date:'yyyy-Www'}}"`). Note that `max` will also add + * native HTML5 constraint validation. + * @param {(date|string)=} ngMin Sets the `min` validation constraint to the Date / ISO week string + * the `ngMin` expression evaluates to. Note that it does not set the `min` attribute. + * @param {(date|string)=} ngMax Sets the `max` validation constraint to the Date / ISO week string + * the `ngMax` expression evaluates to. Note that it does not set the `max` attribute. + * @param {string=} required Sets `required` validation error key if the value is not entered. + * @param {string=} ngRequired Adds `required` attribute and `required` validation constraint to + * the element when the ngRequired expression evaluates to true. Use `ngRequired` instead of + * `required` when you want to data-bind to the `required` attribute. + * @param {string=} ngChange Angular expression to be executed when input changes due to user + * interaction with the input element. + * + * @example + + + +
+ +
+ + Required! + + Not a valid date! +
+ value = {{example.value | date: "yyyy-Www"}}
+ myForm.input.$valid = {{myForm.input.$valid}}
+ myForm.input.$error = {{myForm.input.$error}}
+ myForm.$valid = {{myForm.$valid}}
+ myForm.$error.required = {{!!myForm.$error.required}}
+
+
+ + var value = element(by.binding('example.value | date: "yyyy-Www"')); + var valid = element(by.binding('myForm.input.$valid')); + + // currently protractor/webdriver does not support + // sending keys to all known HTML5 input controls + // for various browsers (https://github.com/angular/protractor/issues/562). + function setInput(val) { + // set the value of the element and force validation. + var scr = "var ipt = document.getElementById('exampleInput'); " + + "ipt.value = '" + val + "';" + + "angular.element(ipt).scope().$apply(function(s) { s.myForm[ipt.name].$setViewValue('" + val + "'); });"; + browser.executeScript(scr); + } + + it('should initialize to model', function() { + expect(value.getText()).toContain('2013-W01'); + expect(valid.getText()).toContain('myForm.input.$valid = true'); + }); + + it('should be invalid if empty', function() { + setInput(''); + expect(value.getText()).toEqual('value ='); + expect(valid.getText()).toContain('myForm.input.$valid = false'); + }); + + it('should be invalid if over max', function() { + setInput('2015-W01'); + expect(value.getText()).toContain(''); + expect(valid.getText()).toContain('myForm.input.$valid = false'); + }); + +
+ */ + 'week': createDateInputType('week', WEEK_REGEXP, weekParser, 'yyyy-Www'), + + /** + * @ngdoc input + * @name input[month] + * + * @description + * Input with month validation and transformation. In browsers that do not yet support + * the HTML5 month input, a text element will be used. In that case, the text must be entered in a valid ISO-8601 + * month format (yyyy-MM), for example: `2009-01`. + * + * The model must always be a Date object, otherwise Angular will throw an error. + * Invalid `Date` objects (dates whose `getTime()` is `NaN`) will be rendered as an empty string. + * If the model is not set to the first of the month, the next view to model update will set it + * to the first of the month. + * + * The timezone to be used to read/write the `Date` instance in the model can be defined using + * {@link ng.directive:ngModelOptions ngModelOptions}. By default, this is the timezone of the browser. + * + * @param {string} ngModel Assignable angular expression to data-bind to. + * @param {string=} name Property name of the form under which the control is published. + * @param {string=} min Sets the `min` validation error key if the value entered is less than `min`. + * This must be a valid ISO month format (yyyy-MM). You can also use interpolation inside this + * attribute (e.g. `min="{{minMonth | date:'yyyy-MM'}}"`). Note that `min` will also add + * native HTML5 constraint validation. + * @param {string=} max Sets the `max` validation error key if the value entered is greater than `max`. + * This must be a valid ISO month format (yyyy-MM). You can also use interpolation inside this + * attribute (e.g. `max="{{maxMonth | date:'yyyy-MM'}}"`). Note that `max` will also add + * native HTML5 constraint validation. + * @param {(date|string)=} ngMin Sets the `min` validation constraint to the Date / ISO week string + * the `ngMin` expression evaluates to. Note that it does not set the `min` attribute. + * @param {(date|string)=} ngMax Sets the `max` validation constraint to the Date / ISO week string + * the `ngMax` expression evaluates to. Note that it does not set the `max` attribute. + + * @param {string=} required Sets `required` validation error key if the value is not entered. + * @param {string=} ngRequired Adds `required` attribute and `required` validation constraint to + * the element when the ngRequired expression evaluates to true. Use `ngRequired` instead of + * `required` when you want to data-bind to the `required` attribute. + * @param {string=} ngChange Angular expression to be executed when input changes due to user + * interaction with the input element. + * + * @example + + + +
+ + +
+ + Required! + + Not a valid month! +
+ value = {{example.value | date: "yyyy-MM"}}
+ myForm.input.$valid = {{myForm.input.$valid}}
+ myForm.input.$error = {{myForm.input.$error}}
+ myForm.$valid = {{myForm.$valid}}
+ myForm.$error.required = {{!!myForm.$error.required}}
+
+
+ + var value = element(by.binding('example.value | date: "yyyy-MM"')); + var valid = element(by.binding('myForm.input.$valid')); + + // currently protractor/webdriver does not support + // sending keys to all known HTML5 input controls + // for various browsers (https://github.com/angular/protractor/issues/562). + function setInput(val) { + // set the value of the element and force validation. + var scr = "var ipt = document.getElementById('exampleInput'); " + + "ipt.value = '" + val + "';" + + "angular.element(ipt).scope().$apply(function(s) { s.myForm[ipt.name].$setViewValue('" + val + "'); });"; + browser.executeScript(scr); + } + + it('should initialize to model', function() { + expect(value.getText()).toContain('2013-10'); + expect(valid.getText()).toContain('myForm.input.$valid = true'); + }); + + it('should be invalid if empty', function() { + setInput(''); + expect(value.getText()).toEqual('value ='); + expect(valid.getText()).toContain('myForm.input.$valid = false'); + }); + + it('should be invalid if over max', function() { + setInput('2015-01'); + expect(value.getText()).toContain(''); + expect(valid.getText()).toContain('myForm.input.$valid = false'); + }); + +
+ */ + 'month': createDateInputType('month', MONTH_REGEXP, + createDateParser(MONTH_REGEXP, ['yyyy', 'MM']), + 'yyyy-MM'), + + /** + * @ngdoc input + * @name input[number] * * @description * Text input with number validation and transformation. Sets the `number` validation * error if not a valid number. * + *
+ * The model must always be of type `number` otherwise Angular will throw an error. + * Be aware that a string containing a number is not enough. See the {@link ngModel:numfmt} + * error docs for more information and an example of how to convert your model if necessary. + *
+ * + * ## Issues with HTML5 constraint validation + * + * In browsers that follow the + * [HTML5 specification](https://html.spec.whatwg.org/multipage/forms.html#number-state-%28type=number%29), + * `input[number]` does not work as expected with {@link ngModelOptions `ngModelOptions.allowInvalid`}. + * If a non-number is entered in the input, the browser will report the value as an empty string, + * which means the view / model values in `ngModel` and subsequently the scope value + * will also be an empty string. + * + * * @param {string} ngModel Assignable angular expression to data-bind to. * @param {string=} name Property name of the form under which the control is published. * @param {string=} min Sets the `min` validation error key if the value entered is less than `min`. + * Can be interpolated. * @param {string=} max Sets the `max` validation error key if the value entered is greater than `max`. + * Can be interpolated. + * @param {string=} ngMin Like `min`, sets the `min` validation error key if the value entered is less than `ngMin`, + * but does not trigger HTML5 native validation. Takes an expression. + * @param {string=} ngMax Like `max`, sets the `max` validation error key if the value entered is greater than `ngMax`, + * but does not trigger HTML5 native validation. Takes an expression. + * @param {string=} step Sets the `step` validation error key if the value entered does not fit the `step` constraint. + * Can be interpolated. + * @param {string=} ngStep Like `step`, sets the `step` validation error key if the value entered does not fit the `ngStep` constraint, + * but does not trigger HTML5 native validation. Takes an expression. * @param {string=} required Sets `required` validation error key if the value is not entered. * @param {string=} ngRequired Adds `required` attribute and `required` validation constraint to * the element when the ngRequired expression evaluates to true. Use `ngRequired` instead of @@ -15721,66 +24554,95 @@ * @param {number=} ngMinlength Sets `minlength` validation error key if the value is shorter than * minlength. * @param {number=} ngMaxlength Sets `maxlength` validation error key if the value is longer than - * maxlength. - * @param {string=} ngPattern Sets `pattern` validation error key if the value does not match the - * RegExp pattern expression. Expected value is `/regexp/` for inline patterns or `regexp` for - * patterns defined as scope expressions. + * maxlength. Setting the attribute to a negative or non-numeric value, allows view values of + * any length. + * @param {string=} pattern Similar to `ngPattern` except that the attribute value is the actual string + * that contains the regular expression body that will be converted to a regular expression + * as in the ngPattern directive. + * @param {string=} ngPattern Sets `pattern` validation error key if the ngModel {@link ngModel.NgModelController#$viewValue $viewValue} + * does not match a RegExp found by evaluating the Angular expression given in the attribute value. + * If the expression evaluates to a RegExp object, then this is used directly. + * If the expression evaluates to a string, then it will be converted to a RegExp + * after wrapping it in `^` and `$` characters. For instance, `"abc"` will be converted to + * `new RegExp('^abc$')`.
+ * **Note:** Avoid using the `g` flag on the RegExp, as it will cause each successive search to + * start at the index of the last search's match, thus not taking the whole input value into + * account. * @param {string=} ngChange Angular expression to be executed when input changes due to user * interaction with the input element. * * @example - - + + -
- Number: + +
Required! Not valid number! - value = {{value}}
+
+ value = {{example.value}}
myForm.input.$valid = {{myForm.input.$valid}}
myForm.input.$error = {{myForm.input.$error}}
myForm.$valid = {{myForm.$valid}}
myForm.$error.required = {{!!myForm.$error.required}}
-
- + + + var value = element(by.binding('example.value')); + var valid = element(by.binding('myForm.input.$valid')); + var input = element(by.model('example.value')); + it('should initialize to model', function() { - expect(binding('value')).toEqual('12'); - expect(binding('myForm.input.$valid')).toEqual('true'); + expect(value.getText()).toContain('12'); + expect(valid.getText()).toContain('true'); }); it('should be invalid if empty', function() { - input('value').enter(''); - expect(binding('value')).toEqual(''); - expect(binding('myForm.input.$valid')).toEqual('false'); + input.clear(); + input.sendKeys(''); + expect(value.getText()).toEqual('value ='); + expect(valid.getText()).toContain('false'); }); it('should be invalid if over max', function() { - input('value').enter('123'); - expect(binding('value')).toEqual(''); - expect(binding('myForm.input.$valid')).toEqual('false'); + input.clear(); + input.sendKeys('123'); + expect(value.getText()).toEqual('value ='); + expect(valid.getText()).toContain('false'); }); - -
+ + */ 'number': numberInputType, /** - * @ngdoc inputType - * @name ng.directive:input.url + * @ngdoc input + * @name input[url] * * @description * Text input with URL validation. Sets the `url` validation error key if the content is not a * valid URL. * + *
+ * **Note:** `input[url]` uses a regex to validate urls that is derived from the regex + * used in Chromium. If you need stricter validation, you can use `ng-pattern` or modify + * the built-in validators (see the {@link guide/forms Forms guide}) + *
+ * * @param {string} ngModel Assignable angular expression to data-bind to. * @param {string=} name Property name of the form under which the control is published. * @param {string=} required Sets `required` validation error key if the value is not entered. @@ -15790,65 +24652,96 @@ * @param {number=} ngMinlength Sets `minlength` validation error key if the value is shorter than * minlength. * @param {number=} ngMaxlength Sets `maxlength` validation error key if the value is longer than - * maxlength. - * @param {string=} ngPattern Sets `pattern` validation error key if the value does not match the - * RegExp pattern expression. Expected value is `/regexp/` for inline patterns or `regexp` for - * patterns defined as scope expressions. + * maxlength. Setting the attribute to a negative or non-numeric value, allows view values of + * any length. + * @param {string=} pattern Similar to `ngPattern` except that the attribute value is the actual string + * that contains the regular expression body that will be converted to a regular expression + * as in the ngPattern directive. + * @param {string=} ngPattern Sets `pattern` validation error key if the ngModel {@link ngModel.NgModelController#$viewValue $viewValue} + * does not match a RegExp found by evaluating the Angular expression given in the attribute value. + * If the expression evaluates to a RegExp object, then this is used directly. + * If the expression evaluates to a string, then it will be converted to a RegExp + * after wrapping it in `^` and `$` characters. For instance, `"abc"` will be converted to + * `new RegExp('^abc$')`.
+ * **Note:** Avoid using the `g` flag on the RegExp, as it will cause each successive search to + * start at the index of the last search's match, thus not taking the whole input value into + * account. * @param {string=} ngChange Angular expression to be executed when input changes due to user * interaction with the input element. * * @example - - + + -
- URL: + +
- + + + var text = element(by.binding('url.text')); + var valid = element(by.binding('myForm.input.$valid')); + var input = element(by.model('url.text')); + it('should initialize to model', function() { - expect(binding('text')).toEqual('http://google.com'); - expect(binding('myForm.input.$valid')).toEqual('true'); + expect(text.getText()).toContain('http://google.com'); + expect(valid.getText()).toContain('true'); }); it('should be invalid if empty', function() { - input('text').enter(''); - expect(binding('text')).toEqual(''); - expect(binding('myForm.input.$valid')).toEqual('false'); + input.clear(); + input.sendKeys(''); + + expect(text.getText()).toEqual('text ='); + expect(valid.getText()).toContain('false'); }); it('should be invalid if not url', function() { - input('text').enter('xxx'); - expect(binding('myForm.input.$valid')).toEqual('false'); + input.clear(); + input.sendKeys('box'); + + expect(valid.getText()).toContain('false'); }); - -
+ + */ 'url': urlInputType, /** - * @ngdoc inputType - * @name ng.directive:input.email + * @ngdoc input + * @name input[email] * * @description * Text input with email validation. Sets the `email` validation error key if not a valid email * address. * + *
+ * **Note:** `input[email]` uses a regex to validate email addresses that is derived from the regex + * used in Chromium. If you need stricter validation (e.g. requiring a top-level domain), you can + * use `ng-pattern` or modify the built-in validators (see the {@link guide/forms Forms guide}) + *
+ * * @param {string} ngModel Assignable angular expression to data-bind to. * @param {string=} name Property name of the form under which the control is published. * @param {string=} required Sets `required` validation error key if the value is not entered. @@ -15858,192 +24751,385 @@ * @param {number=} ngMinlength Sets `minlength` validation error key if the value is shorter than * minlength. * @param {number=} ngMaxlength Sets `maxlength` validation error key if the value is longer than - * maxlength. - * @param {string=} ngPattern Sets `pattern` validation error key if the value does not match the - * RegExp pattern expression. Expected value is `/regexp/` for inline patterns or `regexp` for - * patterns defined as scope expressions. + * maxlength. Setting the attribute to a negative or non-numeric value, allows view values of + * any length. + * @param {string=} pattern Similar to `ngPattern` except that the attribute value is the actual string + * that contains the regular expression body that will be converted to a regular expression + * as in the ngPattern directive. + * @param {string=} ngPattern Sets `pattern` validation error key if the ngModel {@link ngModel.NgModelController#$viewValue $viewValue} + * does not match a RegExp found by evaluating the Angular expression given in the attribute value. + * If the expression evaluates to a RegExp object, then this is used directly. + * If the expression evaluates to a string, then it will be converted to a RegExp + * after wrapping it in `^` and `$` characters. For instance, `"abc"` will be converted to + * `new RegExp('^abc$')`.
+ * **Note:** Avoid using the `g` flag on the RegExp, as it will cause each successive search to + * start at the index of the last search's match, thus not taking the whole input value into + * account. * @param {string=} ngChange Angular expression to be executed when input changes due to user * interaction with the input element. * * @example - - + + -
- Email: + + +
Required! Not valid email! - text = {{text}}
+
+ text = {{email.text}}
myForm.input.$valid = {{myForm.input.$valid}}
myForm.input.$error = {{myForm.input.$error}}
myForm.$valid = {{myForm.$valid}}
myForm.$error.required = {{!!myForm.$error.required}}
myForm.$error.email = {{!!myForm.$error.email}}
-
- + + + var text = element(by.binding('email.text')); + var valid = element(by.binding('myForm.input.$valid')); + var input = element(by.model('email.text')); + it('should initialize to model', function() { - expect(binding('text')).toEqual('me@example.com'); - expect(binding('myForm.input.$valid')).toEqual('true'); - }); + expect(text.getText()).toContain('me@example.com'); + expect(valid.getText()).toContain('true'); + }); it('should be invalid if empty', function() { - input('text').enter(''); - expect(binding('text')).toEqual(''); - expect(binding('myForm.input.$valid')).toEqual('false'); + input.clear(); + input.sendKeys(''); + expect(text.getText()).toEqual('text ='); + expect(valid.getText()).toContain('false'); }); it('should be invalid if not email', function() { - input('text').enter('xxx'); - expect(binding('myForm.input.$valid')).toEqual('false'); + input.clear(); + input.sendKeys('xxx'); + + expect(valid.getText()).toContain('false'); }); - -
+ + */ 'email': emailInputType, /** - * @ngdoc inputType - * @name ng.directive:input.radio + * @ngdoc input + * @name input[radio] * * @description * HTML radio button. * * @param {string} ngModel Assignable angular expression to data-bind to. - * @param {string} value The value to which the expression should be set when selected. + * @param {string} value The value to which the `ngModel` expression should be set when selected. + * Note that `value` only supports `string` values, i.e. the scope model needs to be a string, + * too. Use `ngValue` if you need complex models (`number`, `object`, ...). * @param {string=} name Property name of the form under which the control is published. * @param {string=} ngChange Angular expression to be executed when input changes due to user * interaction with the input element. + * @param {string} ngValue Angular expression to which `ngModel` will be be set when the radio + * is selected. Should be used instead of the `value` attribute if you need + * a non-string `ngModel` (`boolean`, `array`, ...). * * @example - - + + -
- Red
- Green
- Blue
- color = {{color}}
+ +
+
+
+ color = {{color.name | json}}
-
- + Note that `ng-value="specialValue"` sets radio item's value to be the value of `$scope.specialValue`. + + it('should change state', function() { - expect(binding('color')).toEqual('blue'); + var inputs = element.all(by.model('color.name')); + var color = element(by.binding('color.name')); - input('color').select('red'); - expect(binding('color')).toEqual('red'); + expect(color.getText()).toContain('blue'); + + inputs.get(0).click(); + expect(color.getText()).toContain('red'); + + inputs.get(1).click(); + expect(color.getText()).toContain('green'); }); - -
+ + */ 'radio': radioInputType, + /** + * @ngdoc input + * @name input[range] + * + * @description + * Native range input with validation and transformation. + * + * The model for the range input must always be a `Number`. + * + * IE9 and other browsers that do not support the `range` type fall back + * to a text input without any default values for `min`, `max` and `step`. Model binding, + * validation and number parsing are nevertheless supported. + * + * Browsers that support range (latest Chrome, Safari, Firefox, Edge) treat `input[range]` + * in a way that never allows the input to hold an invalid value. That means: + * - any non-numerical value is set to `(max + min) / 2`. + * - any numerical value that is less than the current min val, or greater than the current max val + * is set to the min / max val respectively. + * - additionally, the current `step` is respected, so the nearest value that satisfies a step + * is used. + * + * See the [HTML Spec on input[type=range]](https://www.w3.org/TR/html5/forms.html#range-state-(type=range)) + * for more info. + * + * This has the following consequences for Angular: + * + * Since the element value should always reflect the current model value, a range input + * will set the bound ngModel expression to the value that the browser has set for the + * input element. For example, in the following input ``, + * if the application sets `model.value = null`, the browser will set the input to `'50'`. + * Angular will then set the model to `50`, to prevent input and model value being out of sync. + * + * That means the model for range will immediately be set to `50` after `ngModel` has been + * initialized. It also means a range input can never have the required error. + * + * This does not only affect changes to the model value, but also to the values of the `min`, + * `max`, and `step` attributes. When these change in a way that will cause the browser to modify + * the input value, Angular will also update the model value. + * + * Automatic value adjustment also means that a range input element can never have the `required`, + * `min`, or `max` errors. + * + * However, `step` is currently only fully implemented by Firefox. Other browsers have problems + * when the step value changes dynamically - they do not adjust the element value correctly, but + * instead may set the `stepMismatch` error. If that's the case, the Angular will set the `step` + * error on the input, and set the model to `undefined`. + * + * Note that `input[range]` is not compatible with`ngMax`, `ngMin`, and `ngStep`, because they do + * not set the `min` and `max` attributes, which means that the browser won't automatically adjust + * the input value based on their values, and will always assume min = 0, max = 100, and step = 1. + * + * @param {string} ngModel Assignable angular expression to data-bind to. + * @param {string=} name Property name of the form under which the control is published. + * @param {string=} min Sets the `min` validation to ensure that the value entered is greater + * than `min`. Can be interpolated. + * @param {string=} max Sets the `max` validation to ensure that the value entered is less than `max`. + * Can be interpolated. + * @param {string=} step Sets the `step` validation to ensure that the value entered matches the `step` + * Can be interpolated. + * @param {string=} ngChange Angular expression to be executed when the ngModel value changes due + * to user interaction with the input element. + * @param {expression=} ngChecked If the expression is truthy, then the `checked` attribute will be set on the + * element. **Note** : `ngChecked` should not be used alongside `ngModel`. + * Checkout {@link ng.directive:ngChecked ngChecked} for usage. + * + * @example + + + +
+ + Model as range: +
+ Model as number:
+ Min:
+ Max:
+ value = {{value}}
+ myForm.range.$valid = {{myForm.range.$valid}}
+ myForm.range.$error = {{myForm.range.$error}} +
+
+
+ + * ## Range Input with ngMin & ngMax attributes + + * @example + + + +
+ Model as range: +
+ Model as number:
+ Min:
+ Max:
+ value = {{value}}
+ myForm.range.$valid = {{myForm.range.$valid}}
+ myForm.range.$error = {{myForm.range.$error}} +
+
+
+ + */ + 'range': rangeInputType, /** - * @ngdoc inputType - * @name ng.directive:input.checkbox + * @ngdoc input + * @name input[checkbox] * * @description * HTML checkbox. * * @param {string} ngModel Assignable angular expression to data-bind to. * @param {string=} name Property name of the form under which the control is published. - * @param {string=} ngTrueValue The value to which the expression should be set when selected. - * @param {string=} ngFalseValue The value to which the expression should be set when not selected. + * @param {expression=} ngTrueValue The value to which the expression should be set when selected. + * @param {expression=} ngFalseValue The value to which the expression should be set when not selected. * @param {string=} ngChange Angular expression to be executed when input changes due to user * interaction with the input element. * * @example - - + + -
- Value1:
- Value2:
- value1 = {{value1}}
- value2 = {{value2}}
+ +
+
+ value1 = {{checkboxModel.value1}}
+ value2 = {{checkboxModel.value2}}
-
- + + it('should change state', function() { - expect(binding('value1')).toEqual('true'); - expect(binding('value2')).toEqual('YES'); + var value1 = element(by.binding('checkboxModel.value1')); + var value2 = element(by.binding('checkboxModel.value2')); - input('value1').check(); - input('value2').check(); - expect(binding('value1')).toEqual('false'); - expect(binding('value2')).toEqual('NO'); + expect(value1.getText()).toContain('true'); + expect(value2.getText()).toContain('YES'); + + element(by.model('checkboxModel.value1')).click(); + element(by.model('checkboxModel.value2')).click(); + + expect(value1.getText()).toContain('false'); + expect(value2.getText()).toContain('NO'); }); - -
+ + */ 'checkbox': checkboxInputType, 'hidden': noop, 'button': noop, 'submit': noop, - 'reset': noop + 'reset': noop, + 'file': noop }; -// A helper function to call $setValidity and return the value / undefined, -// a pattern that is repeated a lot in the input validation logic. - function validate(ctrl, validatorName, validity, value){ - ctrl.$setValidity(validatorName, validity); - return validity ? value : undefined; + function stringBasedInputType(ctrl) { + ctrl.$formatters.push(function(value) { + return ctrl.$isEmpty(value) ? value : value.toString(); + }); } function textInputType(scope, element, attr, ctrl, $sniffer, $browser) { - // In composition mode, users are still inputing intermediate text buffer, + baseInputType(scope, element, attr, ctrl, $sniffer, $browser); + stringBasedInputType(ctrl); + } + + function baseInputType(scope, element, attr, ctrl, $sniffer, $browser) { + var type = lowercase(element[0].type); + + // In composition mode, users are still inputting intermediate text buffer, // hold the listener until composition is done. // More about composition events: https://developer.mozilla.org/en-US/docs/Web/API/CompositionEvent if (!$sniffer.android) { var composing = false; - element.on('compositionstart', function(data) { + element.on('compositionstart', function() { composing = true; }); element.on('compositionend', function() { composing = false; + listener(); }); } - var listener = function() { + var timeout; + + var listener = function(ev) { + if (timeout) { + $browser.defer.cancel(timeout); + timeout = null; + } if (composing) return; - var value = element.val(); + var value = element.val(), + event = ev && ev.type; // By default we will trim the value // If the attribute ng-trim exists we will avoid trimming - // e.g. - if (toBoolean(attr.ngTrim || 'T')) { + // If input type is 'password', the value is never trimmed + if (type !== 'password' && (!attr.ngTrim || attr.ngTrim !== 'false')) { value = trim(value); } - if (ctrl.$viewValue !== value) { - if (scope.$$phase) { - ctrl.$setViewValue(value); - } else { - scope.$apply(function() { - ctrl.$setViewValue(value); - }); - } + // If a control is suffering from bad input (due to native validators), browsers discard its + // value, so it may be necessary to revalidate (by calling $setViewValue again) even if the + // control's value is the same empty value twice in a row. + if (ctrl.$viewValue !== value || (value === '' && ctrl.$$hasNativeValidators)) { + ctrl.$setViewValue(value, event); } }; @@ -16052,25 +25138,25 @@ if ($sniffer.hasEvent('input')) { element.on('input', listener); } else { - var timeout; - - var deferListener = function() { + var deferListener = function(ev, input, origValue) { if (!timeout) { timeout = $browser.defer(function() { - listener(); timeout = null; + if (!input || input.value !== origValue) { + listener(ev); + } }); } }; - element.on('keydown', function(event) { + element.on('keydown', /** @this */ function(event) { var key = event.keyCode; // ignore // command modifiers arrows if (key === 91 || (15 < key && key < 19) || (37 <= key && key <= 40)) return; - deferListener(); + deferListener(event, this, this.value); }); // if user modifies input value using context menu in IE, we need "paste" and "cut" events to catch it @@ -16083,176 +25169,559 @@ // or form autocomplete on newer browser, we need "change" event to catch it element.on('change', listener); + // Some native input types (date-family) have the ability to change validity without + // firing any input/change events. + // For these event types, when native validators are present and the browser supports the type, + // check for validity changes on various DOM events. + if (PARTIAL_VALIDATION_TYPES[type] && ctrl.$$hasNativeValidators && type === attr.type) { + element.on(PARTIAL_VALIDATION_EVENTS, /** @this */ function(ev) { + if (!timeout) { + var validity = this[VALIDITY_STATE_PROPERTY]; + var origBadInput = validity.badInput; + var origTypeMismatch = validity.typeMismatch; + timeout = $browser.defer(function() { + timeout = null; + if (validity.badInput !== origBadInput || validity.typeMismatch !== origTypeMismatch) { + listener(ev); + } + }); + } + }); + } + ctrl.$render = function() { - element.val(ctrl.$isEmpty(ctrl.$viewValue) ? '' : ctrl.$viewValue); + // Workaround for Firefox validation #12102. + var value = ctrl.$isEmpty(ctrl.$viewValue) ? '' : ctrl.$viewValue; + if (element.val() !== value) { + element.val(value); + } }; + } - // pattern validator - var pattern = attr.ngPattern, - patternValidator, - match; + function weekParser(isoWeek, existingDate) { + if (isDate(isoWeek)) { + return isoWeek; + } - if (pattern) { - var validateRegex = function(regexp, value) { - return validate(ctrl, 'pattern', ctrl.$isEmpty(value) || regexp.test(value), value); - }; - match = pattern.match(/^\/(.*)\/([gim]*)$/); - if (match) { - pattern = new RegExp(match[1], match[2]); - patternValidator = function(value) { - return validateRegex(pattern, value); - }; - } else { - patternValidator = function(value) { - var patternObj = scope.$eval(pattern); + if (isString(isoWeek)) { + WEEK_REGEXP.lastIndex = 0; + var parts = WEEK_REGEXP.exec(isoWeek); + if (parts) { + var year = +parts[1], + week = +parts[2], + hours = 0, + minutes = 0, + seconds = 0, + milliseconds = 0, + firstThurs = getFirstThursdayOfYear(year), + addDays = (week - 1) * 7; - if (!patternObj || !patternObj.test) { - throw minErr('ngPattern')('noregexp', - 'Expected {0} to be a RegExp but was {1}. Element: {2}', pattern, - patternObj, startingTag(element)); - } - return validateRegex(patternObj, value); - }; + if (existingDate) { + hours = existingDate.getHours(); + minutes = existingDate.getMinutes(); + seconds = existingDate.getSeconds(); + milliseconds = existingDate.getMilliseconds(); + } + + return new Date(year, 0, firstThurs.getDate() + addDays, hours, minutes, seconds, milliseconds); + } + } + + return NaN; + } + + function createDateParser(regexp, mapping) { + return function(iso, date) { + var parts, map; + + if (isDate(iso)) { + return iso; } - ctrl.$formatters.push(patternValidator); - ctrl.$parsers.push(patternValidator); + if (isString(iso)) { + // When a date is JSON'ified to wraps itself inside of an extra + // set of double quotes. This makes the date parsing code unable + // to match the date string and parse it as a date. + if (iso.charAt(0) === '"' && iso.charAt(iso.length - 1) === '"') { + iso = iso.substring(1, iso.length - 1); + } + if (ISO_DATE_REGEXP.test(iso)) { + return new Date(iso); + } + regexp.lastIndex = 0; + parts = regexp.exec(iso); + + if (parts) { + parts.shift(); + if (date) { + map = { + yyyy: date.getFullYear(), + MM: date.getMonth() + 1, + dd: date.getDate(), + HH: date.getHours(), + mm: date.getMinutes(), + ss: date.getSeconds(), + sss: date.getMilliseconds() / 1000 + }; + } else { + map = { yyyy: 1970, MM: 1, dd: 1, HH: 0, mm: 0, ss: 0, sss: 0 }; + } + + forEach(parts, function(part, index) { + if (index < mapping.length) { + map[mapping[index]] = +part; + } + }); + return new Date(map.yyyy, map.MM - 1, map.dd, map.HH, map.mm, map.ss || 0, map.sss * 1000 || 0); + } + } + + return NaN; + }; + } + + function createDateInputType(type, regexp, parseDate, format) { + return function dynamicDateInputType(scope, element, attr, ctrl, $sniffer, $browser, $filter) { + badInputChecker(scope, element, attr, ctrl); + baseInputType(scope, element, attr, ctrl, $sniffer, $browser); + var timezone = ctrl && ctrl.$options.getOption('timezone'); + var previousDate; + + ctrl.$$parserName = type; + ctrl.$parsers.push(function(value) { + if (ctrl.$isEmpty(value)) return null; + if (regexp.test(value)) { + // Note: We cannot read ctrl.$modelValue, as there might be a different + // parser/formatter in the processing chain so that the model + // contains some different data format! + var parsedDate = parseDate(value, previousDate); + if (timezone) { + parsedDate = convertTimezoneToLocal(parsedDate, timezone); + } + return parsedDate; + } + return undefined; + }); + + ctrl.$formatters.push(function(value) { + if (value && !isDate(value)) { + throw ngModelMinErr('datefmt', 'Expected `{0}` to be a date', value); + } + if (isValidDate(value)) { + previousDate = value; + if (previousDate && timezone) { + previousDate = convertTimezoneToLocal(previousDate, timezone, true); + } + return $filter('date')(value, format, timezone); + } else { + previousDate = null; + return ''; + } + }); + + if (isDefined(attr.min) || attr.ngMin) { + var minVal; + ctrl.$validators.min = function(value) { + return !isValidDate(value) || isUndefined(minVal) || parseDate(value) >= minVal; + }; + attr.$observe('min', function(val) { + minVal = parseObservedDateValue(val); + ctrl.$validate(); + }); + } + + if (isDefined(attr.max) || attr.ngMax) { + var maxVal; + ctrl.$validators.max = function(value) { + return !isValidDate(value) || isUndefined(maxVal) || parseDate(value) <= maxVal; + }; + attr.$observe('max', function(val) { + maxVal = parseObservedDateValue(val); + ctrl.$validate(); + }); + } + + function isValidDate(value) { + // Invalid Date: getTime() returns NaN + return value && !(value.getTime && value.getTime() !== value.getTime()); + } + + function parseObservedDateValue(val) { + return isDefined(val) && !isDate(val) ? parseDate(val) || undefined : val; + } + }; + } + + function badInputChecker(scope, element, attr, ctrl) { + var node = element[0]; + var nativeValidation = ctrl.$$hasNativeValidators = isObject(node.validity); + if (nativeValidation) { + ctrl.$parsers.push(function(value) { + var validity = element.prop(VALIDITY_STATE_PROPERTY) || {}; + return validity.badInput || validity.typeMismatch ? undefined : value; + }); + } + } + + function numberFormatterParser(ctrl) { + ctrl.$$parserName = 'number'; + ctrl.$parsers.push(function(value) { + if (ctrl.$isEmpty(value)) return null; + if (NUMBER_REGEXP.test(value)) return parseFloat(value); + return undefined; + }); + + ctrl.$formatters.push(function(value) { + if (!ctrl.$isEmpty(value)) { + if (!isNumber(value)) { + throw ngModelMinErr('numfmt', 'Expected `{0}` to be a number', value); + } + value = value.toString(); + } + return value; + }); + } + + function parseNumberAttrVal(val) { + if (isDefined(val) && !isNumber(val)) { + val = parseFloat(val); + } + return !isNumberNaN(val) ? val : undefined; + } + + function isNumberInteger(num) { + // See http://stackoverflow.com/questions/14636536/how-to-check-if-a-variable-is-an-integer-in-javascript#14794066 + // (minus the assumption that `num` is a number) + + // eslint-disable-next-line no-bitwise + return (num | 0) === num; + } + + function countDecimals(num) { + var numString = num.toString(); + var decimalSymbolIndex = numString.indexOf('.'); + + if (decimalSymbolIndex === -1) { + if (-1 < num && num < 1) { + // It may be in the exponential notation format (`1e-X`) + var match = /e-(\d+)$/.exec(numString); + + if (match) { + return Number(match[1]); + } + } + + return 0; } - // min length validator - if (attr.ngMinlength) { - var minlength = int(attr.ngMinlength); - var minLengthValidator = function(value) { - return validate(ctrl, 'minlength', ctrl.$isEmpty(value) || value.length >= minlength, value); - }; + return numString.length - decimalSymbolIndex - 1; + } - ctrl.$parsers.push(minLengthValidator); - ctrl.$formatters.push(minLengthValidator); + function isValidForStep(viewValue, stepBase, step) { + // At this point `stepBase` and `step` are expected to be non-NaN values + // and `viewValue` is expected to be a valid stringified number. + var value = Number(viewValue); + + var isNonIntegerValue = !isNumberInteger(value); + var isNonIntegerStepBase = !isNumberInteger(stepBase); + var isNonIntegerStep = !isNumberInteger(step); + + // Due to limitations in Floating Point Arithmetic (e.g. `0.3 - 0.2 !== 0.1` or + // `0.5 % 0.1 !== 0`), we need to convert all numbers to integers. + if (isNonIntegerValue || isNonIntegerStepBase || isNonIntegerStep) { + var valueDecimals = isNonIntegerValue ? countDecimals(value) : 0; + var stepBaseDecimals = isNonIntegerStepBase ? countDecimals(stepBase) : 0; + var stepDecimals = isNonIntegerStep ? countDecimals(step) : 0; + + var decimalCount = Math.max(valueDecimals, stepBaseDecimals, stepDecimals); + var multiplier = Math.pow(10, decimalCount); + + value = value * multiplier; + stepBase = stepBase * multiplier; + step = step * multiplier; + + if (isNonIntegerValue) value = Math.round(value); + if (isNonIntegerStepBase) stepBase = Math.round(stepBase); + if (isNonIntegerStep) step = Math.round(step); } - // max length validator - if (attr.ngMaxlength) { - var maxlength = int(attr.ngMaxlength); - var maxLengthValidator = function(value) { - return validate(ctrl, 'maxlength', ctrl.$isEmpty(value) || value.length <= maxlength, value); - }; - - ctrl.$parsers.push(maxLengthValidator); - ctrl.$formatters.push(maxLengthValidator); - } + return (value - stepBase) % step === 0; } function numberInputType(scope, element, attr, ctrl, $sniffer, $browser) { - textInputType(scope, element, attr, ctrl, $sniffer, $browser); + badInputChecker(scope, element, attr, ctrl); + numberFormatterParser(ctrl); + baseInputType(scope, element, attr, ctrl, $sniffer, $browser); - ctrl.$parsers.push(function(value) { - var empty = ctrl.$isEmpty(value); - if (empty || NUMBER_REGEXP.test(value)) { - ctrl.$setValidity('number', true); - return value === '' ? null : (empty ? value : parseFloat(value)); - } else { - ctrl.$setValidity('number', false); - return undefined; + var minVal; + var maxVal; + + if (isDefined(attr.min) || attr.ngMin) { + ctrl.$validators.min = function(value) { + return ctrl.$isEmpty(value) || isUndefined(minVal) || value >= minVal; + }; + + attr.$observe('min', function(val) { + minVal = parseNumberAttrVal(val); + // TODO(matsko): implement validateLater to reduce number of validations + ctrl.$validate(); + }); + } + + if (isDefined(attr.max) || attr.ngMax) { + ctrl.$validators.max = function(value) { + return ctrl.$isEmpty(value) || isUndefined(maxVal) || value <= maxVal; + }; + + attr.$observe('max', function(val) { + maxVal = parseNumberAttrVal(val); + // TODO(matsko): implement validateLater to reduce number of validations + ctrl.$validate(); + }); + } + + if (isDefined(attr.step) || attr.ngStep) { + var stepVal; + ctrl.$validators.step = function(modelValue, viewValue) { + return ctrl.$isEmpty(viewValue) || isUndefined(stepVal) || + isValidForStep(viewValue, minVal || 0, stepVal); + }; + + attr.$observe('step', function(val) { + stepVal = parseNumberAttrVal(val); + // TODO(matsko): implement validateLater to reduce number of validations + ctrl.$validate(); + }); + } + } + + function rangeInputType(scope, element, attr, ctrl, $sniffer, $browser) { + badInputChecker(scope, element, attr, ctrl); + numberFormatterParser(ctrl); + baseInputType(scope, element, attr, ctrl, $sniffer, $browser); + + var supportsRange = ctrl.$$hasNativeValidators && element[0].type === 'range', + minVal = supportsRange ? 0 : undefined, + maxVal = supportsRange ? 100 : undefined, + stepVal = supportsRange ? 1 : undefined, + validity = element[0].validity, + hasMinAttr = isDefined(attr.min), + hasMaxAttr = isDefined(attr.max), + hasStepAttr = isDefined(attr.step); + + var originalRender = ctrl.$render; + + ctrl.$render = supportsRange && isDefined(validity.rangeUnderflow) && isDefined(validity.rangeOverflow) ? + //Browsers that implement range will set these values automatically, but reading the adjusted values after + //$render would cause the min / max validators to be applied with the wrong value + function rangeRender() { + originalRender(); + ctrl.$setViewValue(element.val()); + } : + originalRender; + + if (hasMinAttr) { + ctrl.$validators.min = supportsRange ? + // Since all browsers set the input to a valid value, we don't need to check validity + function noopMinValidator() { return true; } : + // non-support browsers validate the min val + function minValidator(modelValue, viewValue) { + return ctrl.$isEmpty(viewValue) || isUndefined(minVal) || viewValue >= minVal; + }; + + setInitialValueAndObserver('min', minChange); + } + + if (hasMaxAttr) { + ctrl.$validators.max = supportsRange ? + // Since all browsers set the input to a valid value, we don't need to check validity + function noopMaxValidator() { return true; } : + // non-support browsers validate the max val + function maxValidator(modelValue, viewValue) { + return ctrl.$isEmpty(viewValue) || isUndefined(maxVal) || viewValue <= maxVal; + }; + + setInitialValueAndObserver('max', maxChange); + } + + if (hasStepAttr) { + ctrl.$validators.step = supportsRange ? + function nativeStepValidator() { + // Currently, only FF implements the spec on step change correctly (i.e. adjusting the + // input element value to a valid value). It's possible that other browsers set the stepMismatch + // validity error instead, so we can at least report an error in that case. + return !validity.stepMismatch; + } : + // ngStep doesn't set the setp attr, so the browser doesn't adjust the input value as setting step would + function stepValidator(modelValue, viewValue) { + return ctrl.$isEmpty(viewValue) || isUndefined(stepVal) || + isValidForStep(viewValue, minVal || 0, stepVal); + }; + + setInitialValueAndObserver('step', stepChange); + } + + function setInitialValueAndObserver(htmlAttrName, changeFn) { + // interpolated attributes set the attribute value only after a digest, but we need the + // attribute value when the input is first rendered, so that the browser can adjust the + // input value based on the min/max value + element.attr(htmlAttrName, attr[htmlAttrName]); + attr.$observe(htmlAttrName, changeFn); + } + + function minChange(val) { + minVal = parseNumberAttrVal(val); + // ignore changes before model is initialized + if (isNumberNaN(ctrl.$modelValue)) { + return; } - }); - ctrl.$formatters.push(function(value) { - return ctrl.$isEmpty(value) ? '' : '' + value; - }); - - if (attr.min) { - var minValidator = function(value) { - var min = parseFloat(attr.min); - return validate(ctrl, 'min', ctrl.$isEmpty(value) || value >= min, value); - }; - - ctrl.$parsers.push(minValidator); - ctrl.$formatters.push(minValidator); + if (supportsRange) { + var elVal = element.val(); + // IE11 doesn't set the el val correctly if the minVal is greater than the element value + if (minVal > elVal) { + elVal = minVal; + element.val(elVal); + } + ctrl.$setViewValue(elVal); + } else { + // TODO(matsko): implement validateLater to reduce number of validations + ctrl.$validate(); + } } - if (attr.max) { - var maxValidator = function(value) { - var max = parseFloat(attr.max); - return validate(ctrl, 'max', ctrl.$isEmpty(value) || value <= max, value); - }; + function maxChange(val) { + maxVal = parseNumberAttrVal(val); + // ignore changes before model is initialized + if (isNumberNaN(ctrl.$modelValue)) { + return; + } - ctrl.$parsers.push(maxValidator); - ctrl.$formatters.push(maxValidator); + if (supportsRange) { + var elVal = element.val(); + // IE11 doesn't set the el val correctly if the maxVal is less than the element value + if (maxVal < elVal) { + element.val(maxVal); + // IE11 and Chrome don't set the value to the minVal when max < min + elVal = maxVal < minVal ? minVal : maxVal; + } + ctrl.$setViewValue(elVal); + } else { + // TODO(matsko): implement validateLater to reduce number of validations + ctrl.$validate(); + } } - ctrl.$formatters.push(function(value) { - return validate(ctrl, 'number', ctrl.$isEmpty(value) || isNumber(value), value); - }); + function stepChange(val) { + stepVal = parseNumberAttrVal(val); + // ignore changes before model is initialized + if (isNumberNaN(ctrl.$modelValue)) { + return; + } + + // Some browsers don't adjust the input value correctly, but set the stepMismatch error + if (supportsRange && ctrl.$viewValue !== element.val()) { + ctrl.$setViewValue(element.val()); + } else { + // TODO(matsko): implement validateLater to reduce number of validations + ctrl.$validate(); + } + } } function urlInputType(scope, element, attr, ctrl, $sniffer, $browser) { - textInputType(scope, element, attr, ctrl, $sniffer, $browser); + // Note: no badInputChecker here by purpose as `url` is only a validation + // in browsers, i.e. we can always read out input.value even if it is not valid! + baseInputType(scope, element, attr, ctrl, $sniffer, $browser); + stringBasedInputType(ctrl); - var urlValidator = function(value) { - return validate(ctrl, 'url', ctrl.$isEmpty(value) || URL_REGEXP.test(value), value); + ctrl.$$parserName = 'url'; + ctrl.$validators.url = function(modelValue, viewValue) { + var value = modelValue || viewValue; + return ctrl.$isEmpty(value) || URL_REGEXP.test(value); }; - - ctrl.$formatters.push(urlValidator); - ctrl.$parsers.push(urlValidator); } function emailInputType(scope, element, attr, ctrl, $sniffer, $browser) { - textInputType(scope, element, attr, ctrl, $sniffer, $browser); + // Note: no badInputChecker here by purpose as `url` is only a validation + // in browsers, i.e. we can always read out input.value even if it is not valid! + baseInputType(scope, element, attr, ctrl, $sniffer, $browser); + stringBasedInputType(ctrl); - var emailValidator = function(value) { - return validate(ctrl, 'email', ctrl.$isEmpty(value) || EMAIL_REGEXP.test(value), value); + ctrl.$$parserName = 'email'; + ctrl.$validators.email = function(modelValue, viewValue) { + var value = modelValue || viewValue; + return ctrl.$isEmpty(value) || EMAIL_REGEXP.test(value); }; - - ctrl.$formatters.push(emailValidator); - ctrl.$parsers.push(emailValidator); } function radioInputType(scope, element, attr, ctrl) { + var doTrim = !attr.ngTrim || trim(attr.ngTrim) !== 'false'; // make the name unique, if not defined if (isUndefined(attr.name)) { element.attr('name', nextUid()); } - element.on('click', function() { + var listener = function(ev) { + var value; if (element[0].checked) { - scope.$apply(function() { - ctrl.$setViewValue(attr.value); - }); + value = attr.value; + if (doTrim) { + value = trim(value); + } + ctrl.$setViewValue(value, ev && ev.type); } - }); + }; + + element.on('click', listener); ctrl.$render = function() { var value = attr.value; - element[0].checked = (value == ctrl.$viewValue); + if (doTrim) { + value = trim(value); + } + element[0].checked = (value === ctrl.$viewValue); }; attr.$observe('value', ctrl.$render); } - function checkboxInputType(scope, element, attr, ctrl) { - var trueValue = attr.ngTrueValue, - falseValue = attr.ngFalseValue; + function parseConstantExpr($parse, context, name, expression, fallback) { + var parseFn; + if (isDefined(expression)) { + parseFn = $parse(expression); + if (!parseFn.constant) { + throw ngModelMinErr('constexpr', 'Expected constant expression for `{0}`, but saw ' + + '`{1}`.', name, expression); + } + return parseFn(context); + } + return fallback; + } - if (!isString(trueValue)) trueValue = true; - if (!isString(falseValue)) falseValue = false; + function checkboxInputType(scope, element, attr, ctrl, $sniffer, $browser, $filter, $parse) { + var trueValue = parseConstantExpr($parse, scope, 'ngTrueValue', attr.ngTrueValue, true); + var falseValue = parseConstantExpr($parse, scope, 'ngFalseValue', attr.ngFalseValue, false); - element.on('click', function() { - scope.$apply(function() { - ctrl.$setViewValue(element[0].checked); - }); - }); + var listener = function(ev) { + ctrl.$setViewValue(element[0].checked, ev && ev.type); + }; + + element.on('click', listener); ctrl.$render = function() { element[0].checked = ctrl.$viewValue; }; - // Override the standard `$isEmpty` because a value of `false` means empty in a checkbox. + // Override the standard `$isEmpty` because the $viewValue of an empty checkbox is always set to `false` + // This is because of the parser below, which compares the `$modelValue` with `trueValue` to convert + // it to a boolean. ctrl.$isEmpty = function(value) { - return value !== trueValue; + return value === false; }; ctrl.$formatters.push(function(value) { - return value === trueValue; + return equals(value, trueValue); }); ctrl.$parsers.push(function(value) { @@ -16263,7 +25732,7 @@ /** * @ngdoc directive - * @name ng.directive:textarea + * @name textarea * @restrict E * * @description @@ -16280,23 +25749,51 @@ * @param {number=} ngMinlength Sets `minlength` validation error key if the value is shorter than * minlength. * @param {number=} ngMaxlength Sets `maxlength` validation error key if the value is longer than - * maxlength. - * @param {string=} ngPattern Sets `pattern` validation error key if the value does not match the - * RegExp pattern expression. Expected value is `/regexp/` for inline patterns or `regexp` for - * patterns defined as scope expressions. + * maxlength. Setting the attribute to a negative or non-numeric value, allows view values of any + * length. + * @param {string=} ngPattern Sets `pattern` validation error key if the ngModel {@link ngModel.NgModelController#$viewValue $viewValue} + * does not match a RegExp found by evaluating the Angular expression given in the attribute value. + * If the expression evaluates to a RegExp object, then this is used directly. + * If the expression evaluates to a string, then it will be converted to a RegExp + * after wrapping it in `^` and `$` characters. For instance, `"abc"` will be converted to + * `new RegExp('^abc$')`.
+ * **Note:** Avoid using the `g` flag on the RegExp, as it will cause each successive search to + * start at the index of the last search's match, thus not taking the whole input value into + * account. * @param {string=} ngChange Angular expression to be executed when input changes due to user * interaction with the input element. + * @param {boolean=} [ngTrim=true] If set to false Angular will not automatically trim the input. + * + * @knownIssue + * + * When specifying the `placeholder` attribute of ` - - - - it('should data-bind and become invalid', function() { - var contentEditable = element('[contenteditable]'); - - expect(contentEditable.text()).toEqual('Change me!'); - input('userContent').enter(''); - expect(contentEditable.text()).toEqual(''); - expect(contentEditable.prop('className')).toMatch(/ng-invalid-required/); - }); - - * - * - * - */ - var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$parse', - function($scope, $exceptionHandler, $attr, $element, $parse) { - this.$viewValue = Number.NaN; - this.$modelValue = Number.NaN; - this.$parsers = []; - this.$formatters = []; - this.$viewChangeListeners = []; - this.$pristine = true; - this.$dirty = false; - this.$valid = true; - this.$invalid = false; - this.$name = $attr.name; - - var ngModelGet = $parse($attr.ngModel), - ngModelSet = ngModelGet.assign; - - if (!ngModelSet) { - throw minErr('ngModel')('nonassign', "Expression '{0}' is non-assignable. Element: {1}", - $attr.ngModel, startingTag($element)); - } - - /** - * @ngdoc function - * @name ng.directive:ngModel.NgModelController#$render - * @methodOf ng.directive:ngModel.NgModelController - * - * @description - * Called when the view needs to be updated. It is expected that the user of the ng-model - * directive will implement this method. - */ - this.$render = noop; - - /** - * @ngdoc function - * @name { ng.directive:ngModel.NgModelController#$isEmpty - * @methodOf ng.directive:ngModel.NgModelController - * - * @description - * This is called when we need to determine if the value of the input is empty. - * - * For instance, the required directive does this to work out if the input has data or not. - * The default `$isEmpty` function checks whether the value is `undefined`, `''`, `null` or `NaN`. - * - * You can override this for input directives whose concept of being empty is different to the - * default. The `checkboxInputType` directive does this because in its case a value of `false` - * implies empty. - */ - this.$isEmpty = function(value) { - return isUndefined(value) || value === '' || value === null || value !== value; - }; - - var parentForm = $element.inheritedData('$formController') || nullFormCtrl, - invalidCount = 0, // used to easily determine if we are valid - $error = this.$error = {}; // keep invalid keys here - - - // Setup initial state of the control - $element.addClass(PRISTINE_CLASS); - toggleValidCss(true); - - // convenience method for easy toggling of classes - function toggleValidCss(isValid, validationErrorKey) { - validationErrorKey = validationErrorKey ? '-' + snake_case(validationErrorKey, '-') : ''; - $element. - removeClass((isValid ? INVALID_CLASS : VALID_CLASS) + validationErrorKey). - addClass((isValid ? VALID_CLASS : INVALID_CLASS) + validationErrorKey); - } - - /** - * @ngdoc function - * @name ng.directive:ngModel.NgModelController#$setValidity - * @methodOf ng.directive:ngModel.NgModelController - * - * @description - * Change the validity state, and notifies the form when the control changes validity. (i.e. it - * does not notify form if given validator is already marked as invalid). - * - * This method should be called by validators - i.e. the parser or formatter functions. - * - * @param {string} validationErrorKey Name of the validator. the `validationErrorKey` will assign - * to `$error[validationErrorKey]=isValid` so that it is available for data-binding. - * The `validationErrorKey` should be in camelCase and will get converted into dash-case - * for class name. Example: `myError` will result in `ng-valid-my-error` and `ng-invalid-my-error` - * class and can be bound to as `{{someForm.someControl.$error.myError}}` . - * @param {boolean} isValid Whether the current state is valid (true) or invalid (false). - */ - this.$setValidity = function(validationErrorKey, isValid) { - // Purposeful use of ! here to cast isValid to boolean in case it is undefined - // jshint -W018 - if ($error[validationErrorKey] === !isValid) return; - // jshint +W018 - - if (isValid) { - if ($error[validationErrorKey]) invalidCount--; - if (!invalidCount) { - toggleValidCss(true); - this.$valid = true; - this.$invalid = false; - } - } else { - toggleValidCss(false); - this.$invalid = true; - this.$valid = false; - invalidCount++; - } - - $error[validationErrorKey] = !isValid; - toggleValidCss(isValid, validationErrorKey); - - parentForm.$setValidity(validationErrorKey, isValid, this); - }; - - /** - * @ngdoc function - * @name ng.directive:ngModel.NgModelController#$setPristine - * @methodOf ng.directive:ngModel.NgModelController - * - * @description - * Sets the control to its pristine state. - * - * This method can be called to remove the 'ng-dirty' class and set the control to its pristine - * state (ng-pristine class). - */ - this.$setPristine = function () { - this.$dirty = false; - this.$pristine = true; - $element.removeClass(DIRTY_CLASS).addClass(PRISTINE_CLASS); - }; - - /** - * @ngdoc function - * @name ng.directive:ngModel.NgModelController#$setViewValue - * @methodOf ng.directive:ngModel.NgModelController - * - * @description - * Update the view value. - * - * This method should be called when the view value changes, typically from within a DOM event handler. - * For example {@link ng.directive:input input} and - * {@link ng.directive:select select} directives call it. - * - * It will update the $viewValue, then pass this value through each of the functions in `$parsers`, - * which includes any validators. The value that comes out of this `$parsers` pipeline, be applied to - * `$modelValue` and the **expression** specified in the `ng-model` attribute. - * - * Lastly, all the registered change listeners, in the `$viewChangeListeners` list, are called. - * - * Note that calling this function does not trigger a `$digest`. - * - * @param {string} value Value from the view. - */ - this.$setViewValue = function(value) { - this.$viewValue = value; - - // change to dirty - if (this.$pristine) { - this.$dirty = true; - this.$pristine = false; - $element.removeClass(PRISTINE_CLASS).addClass(DIRTY_CLASS); - parentForm.$setDirty(); - } - - forEach(this.$parsers, function(fn) { - value = fn(value); - }); - - if (this.$modelValue !== value) { - this.$modelValue = value; - ngModelSet($scope, value); - forEach(this.$viewChangeListeners, function(listener) { - try { - listener(); - } catch(e) { - $exceptionHandler(e); - } - }); - } - }; - - // model -> value - var ctrl = this; - - $scope.$watch(function ngModelWatch() { - var value = ngModelGet($scope); - - // if scope model value and ngModel value are out of sync - if (ctrl.$modelValue !== value) { - - var formatters = ctrl.$formatters, - idx = formatters.length; - - ctrl.$modelValue = value; - while(idx--) { - value = formatters[idx](value); - } - - if (ctrl.$viewValue !== value) { - ctrl.$viewValue = value; - ctrl.$render(); } } - - return value; - }); + }; }]; - /** - * @ngdoc directive - * @name ng.directive:ngModel - * - * @element input - * - * @description - * The `ngModel` directive binds an `input`,`select`, `textarea` (or custom form control) to a - * property on the scope using {@link ng.directive:ngModel.NgModelController NgModelController}, - * which is created and exposed by this directive. - * - * `ngModel` is responsible for: - * - * - Binding the view into the model, which other directives such as `input`, `textarea` or `select` - * require. - * - Providing validation behavior (i.e. required, number, email, url). - * - Keeping the state of the control (valid/invalid, dirty/pristine, validation errors). - * - Setting related css classes on the element (`ng-valid`, `ng-invalid`, `ng-dirty`, `ng-pristine`). - * - Registering the control with its parent {@link ng.directive:form form}. - * - * Note: `ngModel` will try to bind to the property given by evaluating the expression on the - * current scope. If the property doesn't already exist on this scope, it will be created - * implicitly and added to the scope. - * - * For best practices on using `ngModel`, see: - * - * - {@link https://github.com/angular/angular.js/wiki/Understanding-Scopes} - * - * For basic examples, how to use `ngModel`, see: - * - * - {@link ng.directive:input input} - * - {@link ng.directive:input.text text} - * - {@link ng.directive:input.checkbox checkbox} - * - {@link ng.directive:input.radio radio} - * - {@link ng.directive:input.number number} - * - {@link ng.directive:input.email email} - * - {@link ng.directive:input.url url} - * - {@link ng.directive:select select} - * - {@link ng.directive:textarea textarea} - * - */ - var ngModelDirective = function() { - return { - require: ['ngModel', '^?form'], - controller: NgModelController, - link: function(scope, element, attr, ctrls) { - // notify others, especially parent forms - - var modelCtrl = ctrls[0], - formCtrl = ctrls[1] || nullFormCtrl; - - formCtrl.$addControl(modelCtrl); - - scope.$on('$destroy', function() { - formCtrl.$removeControl(modelCtrl); - }); - } - }; - }; - - - /** - * @ngdoc directive - * @name ng.directive:ngChange - * - * @description - * Evaluate given expression when user changes the input. - * The expression is not evaluated when the value change is coming from the model. - * - * Note, this directive requires `ngModel` to be present. - * - * @element input - * @param {expression} ngChange {@link guide/expression Expression} to evaluate upon change - * in input value. - * - * @example - * - * - * - *
- * - * - *
- * debug = {{confirmed}}
- * counter = {{counter}} - *
- *
- * - * it('should evaluate the expression if changing from view', function() { - * expect(binding('counter')).toEqual('0'); - * element('#ng-change-example1').click(); - * expect(binding('counter')).toEqual('1'); - * expect(binding('confirmed')).toEqual('true'); - * }); - * - * it('should not evaluate the expression if changing from model', function() { - * element('#ng-change-example2').click(); - * expect(binding('counter')).toEqual('0'); - * expect(binding('confirmed')).toEqual('true'); - * }); - * - *
- */ - var ngChangeDirective = valueFn({ - require: 'ngModel', - link: function(scope, element, attr, ctrl) { - ctrl.$viewChangeListeners.push(function() { - scope.$eval(attr.ngChange); - }); - } - }); - - - var requiredDirective = function() { - return { - require: '?ngModel', - link: function(scope, elm, attr, ctrl) { - if (!ctrl) return; - attr.required = true; // force truthy in case we are on non input element - - var validator = function(value) { - if (attr.required && ctrl.$isEmpty(value)) { - ctrl.$setValidity('required', false); - return; - } else { - ctrl.$setValidity('required', true); - return value; - } - }; - - ctrl.$formatters.push(validator); - ctrl.$parsers.unshift(validator); - - attr.$observe('required', function() { - validator(ctrl.$viewValue); - }); - } - }; - }; - - - /** - * @ngdoc directive - * @name ng.directive:ngList - * - * @description - * Text input that converts between a delimited string and an array of strings. The delimiter - * can be a fixed string (by default a comma) or a regular expression. - * - * @element input - * @param {string=} ngList optional delimiter that should be used to split the value. If - * specified in form `/something/` then the value will be converted into a regular expression. - * - * @example - - - -
- List: - - Required! -
- names = {{names}}
- myForm.namesInput.$valid = {{myForm.namesInput.$valid}}
- myForm.namesInput.$error = {{myForm.namesInput.$error}}
- myForm.$valid = {{myForm.$valid}}
- myForm.$error.required = {{!!myForm.$error.required}}
-
-
- - it('should initialize to model', function() { - expect(binding('names')).toEqual('["igor","misko","vojta"]'); - expect(binding('myForm.namesInput.$valid')).toEqual('true'); - expect(element('span.error').css('display')).toBe('none'); - }); - - it('should be invalid if empty', function() { - input('names').enter(''); - expect(binding('names')).toEqual(''); - expect(binding('myForm.namesInput.$valid')).toEqual('false'); - expect(element('span.error').css('display')).not().toBe('none'); - }); - -
- */ - var ngListDirective = function() { - return { - require: 'ngModel', - link: function(scope, element, attr, ctrl) { - var match = /\/(.*)\//.exec(attr.ngList), - separator = match && new RegExp(match[1]) || attr.ngList || ','; - - var parse = function(viewValue) { - // If the viewValue is invalid (say required but empty) it will be `undefined` - if (isUndefined(viewValue)) return; - - var list = []; - - if (viewValue) { - forEach(viewValue.split(separator), function(value) { - if (value) list.push(trim(value)); - }); - } - - return list; - }; - - ctrl.$parsers.push(parse); - ctrl.$formatters.push(function(value) { - if (isArray(value)) { - return value.join(', '); - } - - return undefined; - }); - - // Override the standard $isEmpty because an empty array means the input is empty. - ctrl.$isEmpty = function(value) { - return !value || !value.length; - }; - } - }; - }; - var CONSTANT_VALUE_REGEXP = /^(true|false|\d+)$/; /** * @ngdoc directive - * @name ng.directive:ngValue + * @name ngValue * * @description - * Binds the given expression to the value of `input[select]` or `input[radio]`, so - * that when the element is selected, the `ngModel` of that element is set to the - * bound value. + * Binds the given expression to the value of the element. * - * `ngValue` is useful when dynamically generating lists of radio buttons using `ng-repeat`, as - * shown below. + * It is mainly used on {@link input[radio] `input[radio]`} and option elements, + * so that when the element is selected, the {@link ngModel `ngModel`} of that element (or its + * {@link select `select`} parent element) is set to the bound value. It is especially useful + * for dynamically generated lists using {@link ngRepeat `ngRepeat`}, as shown below. + * + * It can also be used to achieve one-way binding of a given expression to an input element + * such as an `input[text]` or a `textarea`, when that element does not use ngModel. * * @element input * @param {string=} ngValue angular expression, whose value will be bound to the `value` attribute - * of the `input` element + * and `value` property of the element. * * @example - - + + -
+

Which is your favorite?

You chose {{my.favorite}}
-
- + + + var favorite = element(by.binding('my.favorite')); + it('should initialize to model', function() { - expect(binding('my.favorite')).toEqual('unicorns'); + expect(favorite.getText()).toContain('unicorns'); }); it('should bind the values to the inputs', function() { - input('my.favorite').select('pizza'); - expect(binding('my.favorite')).toEqual('pizza'); + element.all(by.model('my.favorite')).get(0).click(); + expect(favorite.getText()).toContain('pizza'); }); - -
+ + */ var ngValueDirective = function() { + /** + * inputs use the value attribute as their default value if the value property is not set. + * Once the value property has been set (by adding input), it will not react to changes to + * the value attribute anymore. Setting both attribute and property fixes this behavior, and + * makes it possible to use ngValue as a sort of one-way bind. + */ + function updateElementValue(element, attr, value) { + // Support: IE9 only + // In IE9 values are converted to string (e.g. `input.value = null` results in `input.value === 'null'`). + var propValue = isDefined(value) ? value : (msie === 9) ? '' : null; + element.prop('value', propValue); + attr.$set('value', value); + } + return { + restrict: 'A', priority: 100, compile: function(tpl, tplAttr) { if (CONSTANT_VALUE_REGEXP.test(tplAttr.ngValue)) { return function ngValueConstantLink(scope, elm, attr) { - attr.$set('value', scope.$eval(attr.ngValue)); + var value = scope.$eval(attr.ngValue); + updateElementValue(elm, attr, value); }; } else { return function ngValueLink(scope, elm, attr) { scope.$watch(attr.ngValue, function valueWatchAction(value) { - attr.$set('value', value); + updateElementValue(elm, attr, value); }); }; } @@ -17041,7 +26028,7 @@ /** * @ngdoc directive - * @name ng.directive:ngBind + * @name ngBind * @restrict AC * * @description @@ -17052,7 +26039,7 @@ * Typically, you don't use `ngBind` directly, but instead you use the double curly markup like * `{{ expression }}` which is similar but less verbose. * - * It is preferrable to use `ngBind` instead of `{{ expression }}` when a template is momentarily + * It is preferable to use `ngBind` instead of `{{ expression }}` if a template is momentarily * displayed by the browser in its raw state before Angular compiles it. Since `ngBind` is an * element attribute, it makes the bindings invisible to the user while the page is loading. * @@ -17065,41 +26052,51 @@ * * @example * Enter a name in the Live Preview text box; the greeting below the text box changes instantly. - - + + -
- Enter name:
+
+
Hello !
- - + + it('should check ng-bind', function() { - expect(using('.doc-example-live').binding('name')).toBe('Whirled'); - using('.doc-example-live').input('name').enter('world'); - expect(using('.doc-example-live').binding('name')).toBe('world'); + var nameInput = element(by.model('name')); + + expect(element(by.binding('name')).getText()).toBe('Whirled'); + nameInput.clear(); + nameInput.sendKeys('world'); + expect(element(by.binding('name')).getText()).toBe('world'); }); - - + + */ - var ngBindDirective = ngDirective(function(scope, element, attr) { - element.addClass('ng-binding').data('$binding', attr.ngBind); - scope.$watch(attr.ngBind, function ngBindWatchAction(value) { - // We are purposefully using == here rather than === because we want to - // catch when value is "null or undefined" - // jshint -W041 - element.text(value == undefined ? '' : value); - }); - }); + var ngBindDirective = ['$compile', function($compile) { + return { + restrict: 'AC', + compile: function ngBindCompile(templateElement) { + $compile.$$addBindingClass(templateElement); + return function ngBindLink(scope, element, attr) { + $compile.$$addBindingInfo(element, attr.ngBind); + element = element[0]; + scope.$watch(attr.ngBind, function ngBindWatchAction(value) { + element.textContent = stringify(value); + }); + }; + } + }; + }]; /** * @ngdoc directive - * @name ng.directive:ngBindTemplate + * @name ngBindTemplate * * @description * The `ngBindTemplate` directive specifies that the element @@ -17115,60 +26112,70 @@ * * @example * Try it here: enter text in text box and watch the greeting change. - - + + -
- Salutation:
- Name:
+
+
+

    
- - + + it('should check ng-bind', function() { - expect(using('.doc-example-live').binding('salutation')). - toBe('Hello'); - expect(using('.doc-example-live').binding('name')). - toBe('World'); - using('.doc-example-live').input('salutation').enter('Greetings'); - using('.doc-example-live').input('name').enter('user'); - expect(using('.doc-example-live').binding('salutation')). - toBe('Greetings'); - expect(using('.doc-example-live').binding('name')). - toBe('user'); + var salutationElem = element(by.binding('salutation')); + var salutationInput = element(by.model('salutation')); + var nameInput = element(by.model('name')); + + expect(salutationElem.getText()).toBe('Hello World!'); + + salutationInput.clear(); + salutationInput.sendKeys('Greetings'); + nameInput.clear(); + nameInput.sendKeys('user'); + + expect(salutationElem.getText()).toBe('Greetings user!'); }); - - + + */ - var ngBindTemplateDirective = ['$interpolate', function($interpolate) { - return function(scope, element, attr) { - // TODO: move this to scenario runner - var interpolateFn = $interpolate(element.attr(attr.$attr.ngBindTemplate)); - element.addClass('ng-binding').data('$binding', interpolateFn); - attr.$observe('ngBindTemplate', function(value) { - element.text(value); - }); + var ngBindTemplateDirective = ['$interpolate', '$compile', function($interpolate, $compile) { + return { + compile: function ngBindTemplateCompile(templateElement) { + $compile.$$addBindingClass(templateElement); + return function ngBindTemplateLink(scope, element, attr) { + var interpolateFn = $interpolate(element.attr(attr.$attr.ngBindTemplate)); + $compile.$$addBindingInfo(element, interpolateFn.expressions); + element = element[0]; + attr.$observe('ngBindTemplate', function(value) { + element.textContent = isUndefined(value) ? '' : value; + }); + }; + } }; }]; /** * @ngdoc directive - * @name ng.directive:ngBindHtml + * @name ngBindHtml * * @description - * Creates a binding that will innerHTML the result of evaluating the `expression` into the current - * element in a secure way. By default, the innerHTML-ed content will be sanitized using the {@link - * ngSanitize.$sanitize $sanitize} service. To utilize this functionality, ensure that `$sanitize` - * is available, for example, by including {@link ngSanitize} in your module's dependencies (not in - * core Angular.) You may also bypass sanitization for values you know are safe. To do so, bind to - * an explicitly trusted value via {@link ng.$sce#methods_trustAsHtml $sce.trustAsHtml}. See the example - * under {@link ng.$sce#Example Strict Contextual Escaping (SCE)}. + * Evaluates the expression and inserts the resulting HTML into the element in a secure way. By default, + * the resulting HTML content will be sanitized using the {@link ngSanitize.$sanitize $sanitize} service. + * To utilize this functionality, ensure that `$sanitize` is available, for example, by including {@link + * ngSanitize} in your module's dependencies (not in core Angular). In order to use {@link ngSanitize} + * in your module's dependencies, you need to include "angular-sanitize.js" in your application. + * + * You may also bypass sanitization for values you know are safe. To do so, bind to + * an explicitly trusted value via {@link ng.$sce#trustAsHtml $sce.trustAsHtml}. See the example + * under {@link ng.$sce#show-me-an-example-using-sce- Strict Contextual Escaping (SCE)}. * * Note: If a `$sanitize` service is unavailable and the bound value isn't explicitly trusted, you * will have an exception (instead of an exploit.) @@ -17177,126 +26184,352 @@ * @param {expression} ngBindHtml {@link guide/expression Expression} to evaluate. * * @example - Try it here: enter text in text box and watch the greeting change. - + -
+

- angular.module('ngBindHtmlExample', ['ngSanitize']) - - .controller('ngBindHtmlCtrl', ['$scope', function ngBindHtmlCtrl($scope) { - $scope.myHTML = - 'I am an HTMLstring with links! and other stuff'; - }]); + angular.module('bindHtmlExample', ['ngSanitize']) + .controller('ExampleController', ['$scope', function($scope) { + $scope.myHTML = + 'I am an HTMLstring with ' + + 'links! and other stuff'; + }]); - + it('should check ng-bind-html', function() { - expect(using('.doc-example-live').binding('myHTML')). - toBe( - 'I am an HTMLstring with links! and other stuff' - ); + expect(element(by.binding('myHTML')).getText()).toBe( + 'I am an HTMLstring with links! and other stuff'); }); */ - var ngBindHtmlDirective = ['$sce', '$parse', function($sce, $parse) { - return function(scope, element, attr) { - element.addClass('ng-binding').data('$binding', attr.ngBindHtml); + var ngBindHtmlDirective = ['$sce', '$parse', '$compile', function($sce, $parse, $compile) { + return { + restrict: 'A', + compile: function ngBindHtmlCompile(tElement, tAttrs) { + var ngBindHtmlGetter = $parse(tAttrs.ngBindHtml); + var ngBindHtmlWatch = $parse(tAttrs.ngBindHtml, function sceValueOf(val) { + // Unwrap the value to compare the actual inner safe value, not the wrapper object. + return $sce.valueOf(val); + }); + $compile.$$addBindingClass(tElement); - var parsed = $parse(attr.ngBindHtml); - function getStringValue() { return (parsed(scope) || '').toString(); } + return function ngBindHtmlLink(scope, element, attr) { + $compile.$$addBindingInfo(element, attr.ngBindHtml); - scope.$watch(getStringValue, function ngBindHtmlWatchAction(value) { - element.html($sce.getTrustedHtml(parsed(scope)) || ''); - }); + scope.$watch(ngBindHtmlWatch, function ngBindHtmlWatchAction() { + // The watched value is the unwrapped value. To avoid re-escaping, use the direct getter. + var value = ngBindHtmlGetter(scope); + element.html($sce.getTrustedHtml(value) || ''); + }); + }; + } }; }]; + /** + * @ngdoc directive + * @name ngChange + * + * @description + * Evaluate the given expression when the user changes the input. + * The expression is evaluated immediately, unlike the JavaScript onchange event + * which only triggers at the end of a change (usually, when the user leaves the + * form element or presses the return key). + * + * The `ngChange` expression is only evaluated when a change in the input value causes + * a new value to be committed to the model. + * + * It will not be evaluated: + * * if the value returned from the `$parsers` transformation pipeline has not changed + * * if the input has continued to be invalid since the model will stay `null` + * * if the model is changed programmatically and not by a change to the input value + * + * + * Note, this directive requires `ngModel` to be present. + * + * @element input + * @param {expression} ngChange {@link guide/expression Expression} to evaluate upon change + * in input value. + * + * @example + * + * + * + *
+ * + * + *
+ * debug = {{confirmed}}
+ * counter = {{counter}}
+ *
+ *
+ * + * var counter = element(by.binding('counter')); + * var debug = element(by.binding('confirmed')); + * + * it('should evaluate the expression if changing from view', function() { + * expect(counter.getText()).toContain('0'); + * + * element(by.id('ng-change-example1')).click(); + * + * expect(counter.getText()).toContain('1'); + * expect(debug.getText()).toContain('true'); + * }); + * + * it('should not evaluate the expression if changing from model', function() { + * element(by.id('ng-change-example2')).click(); + + * expect(counter.getText()).toContain('0'); + * expect(debug.getText()).toContain('true'); + * }); + * + *
+ */ + var ngChangeDirective = valueFn({ + restrict: 'A', + require: 'ngModel', + link: function(scope, element, attr, ctrl) { + ctrl.$viewChangeListeners.push(function() { + scope.$eval(attr.ngChange); + }); + } + }); + + /* exported + ngClassDirective, + ngClassEvenDirective, + ngClassOddDirective +*/ + function classDirective(name, selector) { name = 'ngClass' + name; - return function() { + var indexWatchExpression; + + return ['$parse', function($parse) { return { restrict: 'AC', link: function(scope, element, attr) { - var oldVal; + var expression = attr[name].trim(); + var isOneTime = (expression.charAt(0) === ':') && (expression.charAt(1) === ':'); - scope.$watch(attr[name], ngClassWatchAction, true); + var watchInterceptor = isOneTime ? toFlatValue : toClassString; + var watchExpression = $parse(expression, watchInterceptor); + var watchAction = isOneTime ? ngClassOneTimeWatchAction : ngClassWatchAction; - attr.$observe('class', function(value) { - ngClassWatchAction(scope.$eval(attr[name])); - }); + var classCounts = element.data('$classCounts'); + var oldModulo = true; + var oldClassString; + if (!classCounts) { + // Use createMap() to prevent class assumptions involving property + // names in Object.prototype + classCounts = createMap(); + element.data('$classCounts', classCounts); + } if (name !== 'ngClass') { - scope.$watch('$index', function($index, old$index) { - // jshint bitwise: false - var mod = $index & 1; - if (mod !== old$index & 1) { - var classes = flattenClasses(scope.$eval(attr[name])); - mod === selector ? - attr.$addClass(classes) : - attr.$removeClass(classes); + if (!indexWatchExpression) { + indexWatchExpression = $parse('$index', function moduloTwo($index) { + // eslint-disable-next-line no-bitwise + return $index & 1; + }); + } + + scope.$watch(indexWatchExpression, ngClassIndexWatchAction); + } + + scope.$watch(watchExpression, watchAction, isOneTime); + + function addClasses(classString) { + classString = digestClassCounts(split(classString), 1); + attr.$addClass(classString); + } + + function removeClasses(classString) { + classString = digestClassCounts(split(classString), -1); + attr.$removeClass(classString); + } + + function updateClasses(oldClassString, newClassString) { + var oldClassArray = split(oldClassString); + var newClassArray = split(newClassString); + + var toRemoveArray = arrayDifference(oldClassArray, newClassArray); + var toAddArray = arrayDifference(newClassArray, oldClassArray); + + var toRemoveString = digestClassCounts(toRemoveArray, -1); + var toAddString = digestClassCounts(toAddArray, 1); + + attr.$addClass(toAddString); + attr.$removeClass(toRemoveString); + } + + function digestClassCounts(classArray, count) { + var classesToUpdate = []; + + forEach(classArray, function(className) { + if (count > 0 || classCounts[className]) { + classCounts[className] = (classCounts[className] || 0) + count; + if (classCounts[className] === +(count > 0)) { + classesToUpdate.push(className); + } } }); + + return classesToUpdate.join(' '); } - - function ngClassWatchAction(newVal) { - if (selector === true || scope.$index % 2 === selector) { - var newClasses = flattenClasses(newVal || ''); - if(!oldVal) { - attr.$addClass(newClasses); - } else if(!equals(newVal,oldVal)) { - attr.$updateClass(newClasses, flattenClasses(oldVal)); - } + function ngClassIndexWatchAction(newModulo) { + // This watch-action should run before the `ngClass[OneTime]WatchAction()`, thus it + // adds/removes `oldClassString`. If the `ngClass` expression has changed as well, the + // `ngClass[OneTime]WatchAction()` will update the classes. + if (newModulo === selector) { + addClasses(oldClassString); + } else { + removeClasses(oldClassString); } - oldVal = copy(newVal); + + oldModulo = newModulo; } + function ngClassOneTimeWatchAction(newClassValue) { + var newClassString = toClassString(newClassValue); - function flattenClasses(classVal) { - if(isArray(classVal)) { - return classVal.join(' '); - } else if (isObject(classVal)) { - var classes = [], i = 0; - forEach(classVal, function(v, k) { - if (v) { - classes.push(k); - } - }); - return classes.join(' '); + if (newClassString !== oldClassString) { + ngClassWatchAction(newClassString); + } + } + + function ngClassWatchAction(newClassString) { + if (oldModulo === selector) { + updateClasses(oldClassString, newClassString); } - return classVal; + oldClassString = newClassString; } } }; - }; + }]; + + // Helpers + function arrayDifference(tokens1, tokens2) { + if (!tokens1 || !tokens1.length) return []; + if (!tokens2 || !tokens2.length) return tokens1; + + var values = []; + + outer: + for (var i = 0; i < tokens1.length; i++) { + var token = tokens1[i]; + for (var j = 0; j < tokens2.length; j++) { + if (token === tokens2[j]) continue outer; + } + values.push(token); + } + + return values; + } + + function split(classString) { + return classString && classString.split(' '); + } + + function toClassString(classValue) { + var classString = classValue; + + if (isArray(classValue)) { + classString = classValue.map(toClassString).join(' '); + } else if (isObject(classValue)) { + classString = Object.keys(classValue). + filter(function(key) { return classValue[key]; }). + join(' '); + } + + return classString; + } + + function toFlatValue(classValue) { + var flatValue = classValue; + + if (isArray(classValue)) { + flatValue = classValue.map(toFlatValue); + } else if (isObject(classValue)) { + var hasUndefined = false; + + flatValue = Object.keys(classValue).filter(function(key) { + var value = classValue[key]; + + if (!hasUndefined && isUndefined(value)) { + hasUndefined = true; + } + + return value; + }); + + if (hasUndefined) { + // Prevent the `oneTimeLiteralWatchInterceptor` from unregistering + // the watcher, by including at least one `undefined` value. + flatValue.push(undefined); + } + } + + return flatValue; + } } /** * @ngdoc directive - * @name ng.directive:ngClass + * @name ngClass * @restrict AC * * @description * The `ngClass` directive allows you to dynamically set CSS classes on an HTML element by databinding * an expression that represents all classes to be added. * + * The directive operates in three different ways, depending on which of three types the expression + * evaluates to: + * + * 1. If the expression evaluates to a string, the string should be one or more space-delimited class + * names. + * + * 2. If the expression evaluates to an object, then for each key-value pair of the + * object with a truthy value the corresponding key is used as a class name. + * + * 3. If the expression evaluates to an array, each element of the array should either be a string as in + * type 1 or an object as in type 2. This means that you can mix strings and objects together in an array + * to give you more control over what CSS classes appear. See the code below for an example of this. + * + * * The directive won't add duplicate classes if a particular class was already set. * - * When the expression changes, the previously added classes are removed and only then the - * new classes are added. + * When the expression changes, the previously added classes are removed and only then are the + * new classes added. + * + * @knownIssue + * You should not use {@link guide/interpolation interpolation} in the value of the `class` + * attribute, when using the `ngClass` directive on the same element. + * See {@link guide/interpolation#known-issues here} for more info. * * @animations - * add - happens just before the class is applied to the element - * remove - happens just before the class is removed from the element + * | Animation | Occurs | + * |----------------------------------|-------------------------------------| + * | {@link ng.$animate#addClass addClass} | just before the class is applied to the element | + * | {@link ng.$animate#removeClass removeClass} | just before the class is removed from the element | * * @element ANY * @param {expression} ngClass {@link guide/expression Expression} to eval. The result @@ -17306,24 +26539,41 @@ * element. * * @example Example that demonstrates basic bindings via ngClass directive. - + -

Map Syntax Example

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

Map Syntax Example

+
+
+

Using String Syntax

- +

Using Array Syntax

-
-
-
+
+
+
+
+

Using Array and Map Syntax

+
+
.strike { - text-decoration: line-through; + text-decoration: line-through; } .bold { font-weight: bold; @@ -17331,32 +26581,49 @@ .red { color: red; } + .has-error { + color: red; + background-color: yellow; + } + .orange { + color: orange; + } - + + var ps = element.all(by.css('p')); + it('should let you toggle the class', function() { - expect(element('.doc-example-live p:first').prop('className')).not().toMatch(/bold/); - expect(element('.doc-example-live p:first').prop('className')).not().toMatch(/red/); + expect(ps.first().getAttribute('class')).not.toMatch(/bold/); + expect(ps.first().getAttribute('class')).not.toMatch(/has-error/); - input('important').check(); - expect(element('.doc-example-live p:first').prop('className')).toMatch(/bold/); + element(by.model('important')).click(); + expect(ps.first().getAttribute('class')).toMatch(/bold/); - input('error').check(); - expect(element('.doc-example-live p:first').prop('className')).toMatch(/red/); + element(by.model('error')).click(); + expect(ps.first().getAttribute('class')).toMatch(/has-error/); }); it('should let you toggle string example', function() { - expect(element('.doc-example-live p:nth-of-type(2)').prop('className')).toBe(''); - input('style').enter('red'); - expect(element('.doc-example-live p:nth-of-type(2)').prop('className')).toBe('red'); + expect(ps.get(1).getAttribute('class')).toBe(''); + element(by.model('style')).clear(); + element(by.model('style')).sendKeys('red'); + expect(ps.get(1).getAttribute('class')).toBe('red'); }); it('array example should have 3 classes', function() { - expect(element('.doc-example-live p:last').prop('className')).toBe(''); - input('style1').enter('bold'); - input('style2').enter('strike'); - input('style3').enter('red'); - expect(element('.doc-example-live p:last').prop('className')).toBe('bold strike red'); + expect(ps.get(2).getAttribute('class')).toBe(''); + element(by.model('style1')).sendKeys('bold'); + element(by.model('style2')).sendKeys('strike'); + element(by.model('style3')).sendKeys('red'); + expect(ps.get(2).getAttribute('class')).toBe('bold strike red'); + }); + + it('array with map example should have 2 classes', function() { + expect(ps.last().getAttribute('class')).toBe(''); + element(by.model('style4')).sendKeys('bold'); + element(by.model('warning')).click(); + expect(ps.last().getAttribute('class')).toBe('bold orange'); });
@@ -17365,16 +26632,15 @@ The example below demonstrates how to perform animations using ngClass. - + - - + +
Sample Text
.base-class { - -webkit-transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s; transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s; } @@ -17383,19 +26649,19 @@ font-size:3em; } - + it('should check ng-class', function() { - expect(element('.doc-example-live span').prop('className')).not(). + expect(element(by.css('.base-class')).getAttribute('class')).not. toMatch(/my-class/); - using('.doc-example-live').element(':button:first').click(); + element(by.id('setbtn')).click(); - expect(element('.doc-example-live span').prop('className')). + expect(element(by.css('.base-class')).getAttribute('class')). toMatch(/my-class/); - using('.doc-example-live').element(':button:last').click(); + element(by.id('clearbtn')).click(); - expect(element('.doc-example-live span').prop('className')).not(). + expect(element(by.css('.base-class')).getAttribute('class')).not. toMatch(/my-class/); }); @@ -17406,14 +26672,14 @@ The ngClass directive still supports CSS3 Transitions/Animations even if they do not follow the ngAnimate CSS naming structure. Upon animation ngAnimate will apply supplementary CSS classes to track the start and end of an animation, but this will not hinder any pre-existing CSS transitions already on the element. To get an idea of what happens during a class-based animation, be sure - to view the step by step details of {@link ngAnimate.$animate#methods_addclass $animate.addClass} and - {@link ngAnimate.$animate#methods_removeclass $animate.removeClass}. + to view the step by step details of {@link $animate#addClass $animate.addClass} and + {@link $animate#removeClass $animate.removeClass}. */ var ngClassDirective = classDirective('', true); /** * @ngdoc directive - * @name ng.directive:ngClassOdd + * @name ngClassOdd * @restrict AC * * @description @@ -17429,7 +26695,7 @@ * of the evaluation can be a string representing space delimited class names or an array. * * @example - +
  1. @@ -17447,11 +26713,11 @@ color: blue; } - + it('should check ng-class-odd and ng-class-even', function() { - expect(element('.doc-example-live li:first span').prop('className')). + expect(element(by.repeater('name in names').row(0).column('name')).getAttribute('class')). toMatch(/odd/); - expect(element('.doc-example-live li:last span').prop('className')). + expect(element(by.repeater('name in names').row(1).column('name')).getAttribute('class')). toMatch(/even/); }); @@ -17461,7 +26727,7 @@ /** * @ngdoc directive - * @name ng.directive:ngClassEven + * @name ngClassEven * @restrict AC * * @description @@ -17477,7 +26743,7 @@ * result of the evaluation can be a string representing space delimited class names or an array. * * @example - +
    1. @@ -17495,11 +26761,11 @@ color: blue; } - + it('should check ng-class-odd and ng-class-even', function() { - expect(element('.doc-example-live li:first span').prop('className')). + expect(element(by.repeater('name in names').row(0).column('name')).getAttribute('class')). toMatch(/odd/); - expect(element('.doc-example-live li:last span').prop('className')). + expect(element(by.repeater('name in names').row(1).column('name')).getAttribute('class')). toMatch(/even/); }); @@ -17509,7 +26775,7 @@ /** * @ngdoc directive - * @name ng.directive:ngCloak + * @name ngCloak * @restrict AC * * @description @@ -17525,11 +26791,11 @@ * `angular.min.js`. * For CSP mode please add `angular-csp.css` to your html file (see {@link ng.directive:ngCsp ngCsp}). * - *
      +   * ```css
          * [ng\:cloak], [ng-cloak], [data-ng-cloak], [x-ng-cloak], .ng-cloak, .x-ng-cloak {
        *   display: none !important;
        * }
      -   * 
      + * ``` * * When this css rule is loaded by the browser, all html elements (including their children) that * are tagged with the `ngCloak` directive are hidden. When Angular encounters this directive @@ -17540,27 +26806,23 @@ * document; alternatively, the css rule above must be included in the external stylesheet of the * application. * - * Legacy browsers, like IE7, do not provide attribute selector support (added in CSS 2.1) so they - * cannot match the `[ng\:cloak]` selector. To work around this limitation, you must add the css - * class `ng-cloak` in addition to the `ngCloak` directive as shown in the example below. - * * @element ANY * * @example - - + +
      {{ 'hello' }}
      -
      {{ 'hello IE7' }}
      -
      - +
      {{ 'world' }}
      +
      + it('should remove the template directive and css class', function() { - expect(element('.doc-example-live #template1').attr('ng-cloak')). - not().toBeDefined(); - expect(element('.doc-example-live #template2').attr('ng-cloak')). - not().toBeDefined(); + expect($('#template1').getAttribute('ng-cloak')). + toBeNull(); + expect($('#template2').getAttribute('ng-cloak')). + toBeNull(); }); - - + + * */ var ngCloakDirective = ngDirective({ @@ -17572,7 +26834,7 @@ /** * @ngdoc directive - * @name ng.directive:ngController + * @name ngController * * @description * The `ngController` directive attaches a controller class to the view. This is a key aspect of how angular @@ -17580,7 +26842,7 @@ * * MVC components in angular: * - * * Model — The Model is scope properties; scopes are attached to the DOM where scope properties + * * Model — Models are the properties of a scope; scopes are attached to the DOM where scope properties * are accessed through bindings. * * View — The template (HTML with data bindings) that is rendered into the View. * * Controller — The `ngController` directive specifies a Controller class; the class contains business @@ -17593,149 +26855,214 @@ * * @element ANY * @scope - * @param {expression} ngController Name of a globally accessible constructor function or an - * {@link guide/expression expression} that on the current scope evaluates to a - * constructor function. The controller instance can be published into a scope property - * by specifying `as propertyName`. + * @priority 500 + * @param {expression} ngController Name of a constructor function registered with the current + * {@link ng.$controllerProvider $controllerProvider} or an {@link guide/expression expression} + * that on the current scope evaluates to a constructor function. + * + * The controller instance can be published into a scope property by specifying + * `ng-controller="as propertyName"`. + * + * If the current `$controllerProvider` is configured to use globals (via + * {@link ng.$controllerProvider#allowGlobals `$controllerProvider.allowGlobals()` }), this may + * also be the name of a globally accessible constructor function (deprecated, not recommended). * * @example * Here is a simple form for editing user contact information. Adding, removing, clearing, and * greeting are methods declared on the controller (see source tab). These methods can - * easily be called from the angular markup. Notice that the scope becomes the `this` for the - * controller's instance. This allows for easy access to the view data from the controller. Also - * notice that any changes to the data are automatically reflected in the View without the need - * for a manual update. The example is shown in two different declaration styles you may use - * according to preference. - - - -
      - Name: - [ greet ]
      - Contact: -
        -
      • - - - [ clear - | X ] -
      • -
      • [ add ]
      • -
      -
      -
      - - it('should check controller as', function() { - expect(element('#ctrl-as-exmpl>:input').val()).toBe('John Smith'); - expect(element('#ctrl-as-exmpl li:nth-child(1) input').val()) - .toBe('408 555 1212'); - expect(element('#ctrl-as-exmpl li:nth-child(2) input').val()) - .toBe('john.smith@example.org'); - - element('#ctrl-as-exmpl li:first a:contains("clear")').click(); - expect(element('#ctrl-as-exmpl li:first input').val()).toBe(''); - - element('#ctrl-as-exmpl li:last a:contains("add")').click(); - expect(element('#ctrl-as-exmpl li:nth-child(3) input').val()) - .toBe('yourname@example.org'); - }); - -
      - - - -
      - Name: - [ greet ]
      - Contact: -
        -
      • - - - [ clear - | X ] -
      • -
      • [ add ]
      • -
      -
      -
      - - it('should check controller', function() { - expect(element('#ctrl-exmpl>:input').val()).toBe('John Smith'); - expect(element('#ctrl-exmpl li:nth-child(1) input').val()) - .toBe('408 555 1212'); - expect(element('#ctrl-exmpl li:nth-child(2) input').val()) - .toBe('john.smith@example.org'); - - element('#ctrl-exmpl li:first a:contains("clear")').click(); - expect(element('#ctrl-exmpl li:first input').val()).toBe(''); - - element('#ctrl-exmpl li:last a:contains("add")').click(); - expect(element('#ctrl-exmpl li:nth-child(3) input').val()) - .toBe('yourname@example.org'); - }); - -
      + * easily be called from the angular markup. Any changes to the data are automatically reflected + * in the View without the need for a manual update. + * + * Two different declaration styles are included below: + * + * * one binds methods and properties directly onto the controller using `this`: + * `ng-controller="SettingsController1 as settings"` + * * one injects `$scope` into the controller: + * `ng-controller="SettingsController2"` + * + * The second option is more common in the Angular community, and is generally used in boilerplates + * and in this guide. However, there are advantages to binding properties directly to the controller + * and avoiding scope. + * + * * Using `controller as` makes it obvious which controller you are accessing in the template when + * multiple controllers apply to an element. + * * If you are writing your controllers as classes you have easier access to the properties and + * methods, which will appear on the scope, from inside the controller code. + * * Since there is always a `.` in the bindings, you don't have to worry about prototypal + * inheritance masking primitives. + * + * This example demonstrates the `controller as` syntax. + * + * + * + *
      + * + *
      + * Contact: + *
        + *
      • + * + * + * + * + *
      • + *
      • + *
      + *
      + *
      + * + * angular.module('controllerAsExample', []) + * .controller('SettingsController1', SettingsController1); + * + * function SettingsController1() { + * this.name = 'John Smith'; + * this.contacts = [ + * {type: 'phone', value: '408 555 1212'}, + * {type: 'email', value: 'john.smith@example.org'} + * ]; + * } + * + * SettingsController1.prototype.greet = function() { + * alert(this.name); + * }; + * + * SettingsController1.prototype.addContact = function() { + * this.contacts.push({type: 'email', value: 'yourname@example.org'}); + * }; + * + * SettingsController1.prototype.removeContact = function(contactToRemove) { + * var index = this.contacts.indexOf(contactToRemove); + * this.contacts.splice(index, 1); + * }; + * + * SettingsController1.prototype.clearContact = function(contact) { + * contact.type = 'phone'; + * contact.value = ''; + * }; + * + * + * it('should check controller as', function() { + * var container = element(by.id('ctrl-as-exmpl')); + * expect(container.element(by.model('settings.name')) + * .getAttribute('value')).toBe('John Smith'); + * + * var firstRepeat = + * container.element(by.repeater('contact in settings.contacts').row(0)); + * var secondRepeat = + * container.element(by.repeater('contact in settings.contacts').row(1)); + * + * expect(firstRepeat.element(by.model('contact.value')).getAttribute('value')) + * .toBe('408 555 1212'); + * + * expect(secondRepeat.element(by.model('contact.value')).getAttribute('value')) + * .toBe('john.smith@example.org'); + * + * firstRepeat.element(by.buttonText('clear')).click(); + * + * expect(firstRepeat.element(by.model('contact.value')).getAttribute('value')) + * .toBe(''); + * + * container.element(by.buttonText('add')).click(); + * + * expect(container.element(by.repeater('contact in settings.contacts').row(2)) + * .element(by.model('contact.value')) + * .getAttribute('value')) + * .toBe('yourname@example.org'); + * }); + * + *
      + * + * This example demonstrates the "attach to `$scope`" style of controller. + * + * + * + *
      + * + *
      + * Contact: + *
        + *
      • + * + * + * + * + *
      • + *
      • [ ]
      • + *
      + *
      + *
      + * + * angular.module('controllerExample', []) + * .controller('SettingsController2', ['$scope', SettingsController2]); + * + * function SettingsController2($scope) { + * $scope.name = 'John Smith'; + * $scope.contacts = [ + * {type:'phone', value:'408 555 1212'}, + * {type:'email', value:'john.smith@example.org'} + * ]; + * + * $scope.greet = function() { + * alert($scope.name); + * }; + * + * $scope.addContact = function() { + * $scope.contacts.push({type:'email', value:'yourname@example.org'}); + * }; + * + * $scope.removeContact = function(contactToRemove) { + * var index = $scope.contacts.indexOf(contactToRemove); + * $scope.contacts.splice(index, 1); + * }; + * + * $scope.clearContact = function(contact) { + * contact.type = 'phone'; + * contact.value = ''; + * }; + * } + * + * + * it('should check controller', function() { + * var container = element(by.id('ctrl-exmpl')); + * + * expect(container.element(by.model('name')) + * .getAttribute('value')).toBe('John Smith'); + * + * var firstRepeat = + * container.element(by.repeater('contact in contacts').row(0)); + * var secondRepeat = + * container.element(by.repeater('contact in contacts').row(1)); + * + * expect(firstRepeat.element(by.model('contact.value')).getAttribute('value')) + * .toBe('408 555 1212'); + * expect(secondRepeat.element(by.model('contact.value')).getAttribute('value')) + * .toBe('john.smith@example.org'); + * + * firstRepeat.element(by.buttonText('clear')).click(); + * + * expect(firstRepeat.element(by.model('contact.value')).getAttribute('value')) + * .toBe(''); + * + * container.element(by.buttonText('add')).click(); + * + * expect(container.element(by.repeater('contact in contacts').row(2)) + * .element(by.model('contact.value')) + * .getAttribute('value')) + * .toBe('yourname@example.org'); + * }); + * + *
      */ var ngControllerDirective = [function() { return { + restrict: 'A', scope: true, controller: '@', priority: 500 @@ -17744,389 +27071,604 @@ /** * @ngdoc directive - * @name ng.directive:ngCsp + * @name ngCsp * - * @element html + * @restrict A + * @element ANY * @description - * Enables [CSP (Content Security Policy)](https://developer.mozilla.org/en/Security/CSP) support. * - * This is necessary when developing things like Google Chrome Extensions. + * Angular has some features that can conflict with certain restrictions that are applied when using + * [CSP (Content Security Policy)](https://developer.mozilla.org/en/Security/CSP) rules. * - * CSP forbids apps to use `eval` or `Function(string)` generated functions (among other things). - * For us to be compatible, we just need to implement the "getterFn" in $parse without violating - * any of these restrictions. + * If you intend to implement CSP with these rules then you must tell Angular not to use these + * features. * - * AngularJS uses `Function(string)` generated functions as a speed optimization. Applying the `ngCsp` - * directive will cause Angular to use CSP compatibility mode. When this mode is on AngularJS will - * evaluate all expressions up to 30% slower than in non-CSP mode, but no security violations will - * be raised. + * This is necessary when developing things like Google Chrome Extensions or Universal Windows Apps. * - * CSP forbids JavaScript to inline stylesheet rules. In non CSP mode Angular automatically - * includes some CSS rules (e.g. {@link ng.directive:ngCloak ngCloak}). - * To make those directives work in CSP mode, include the `angular-csp.css` manually. * - * In order to use this feature put the `ngCsp` directive on the root element of the application. + * The following default rules in CSP affect Angular: + * + * * The use of `eval()`, `Function(string)` and similar functions to dynamically create and execute + * code from strings is forbidden. Angular makes use of this in the {@link $parse} service to + * provide a 30% increase in the speed of evaluating Angular expressions. (This CSP rule can be + * disabled with the CSP keyword `unsafe-eval`, but it is generally not recommended as it would + * weaken the protections offered by CSP.) + * + * * The use of inline resources, such as inline ` -
      + Enter text and hit enter:
      list={{list}}
      - - + + it('should check ng-submit', function() { - expect(binding('list')).toBe('[]'); - element('.doc-example-live #submit').click(); - expect(binding('list')).toBe('["hello"]'); - expect(input('text').val()).toBe(''); + expect(element(by.binding('list')).getText()).toBe('list=[]'); + element(by.css('#submit')).click(); + expect(element(by.binding('list')).getText()).toContain('hello'); + expect(element(by.model('text')).getAttribute('value')).toBe(''); }); it('should ignore empty strings', function() { - expect(binding('list')).toBe('[]'); - element('.doc-example-live #submit').click(); - element('.doc-example-live #submit').click(); - expect(binding('list')).toBe('["hello"]'); - }); - - + expect(element(by.binding('list')).getText()).toBe('list=[]'); + element(by.css('#submit')).click(); + element(by.css('#submit')).click(); + expect(element(by.binding('list')).getText()).toContain('hello'); + }); + + */ /** * @ngdoc directive - * @name ng.directive:ngFocus + * @name ngFocus * * @description * Specify custom behavior on focus event. * + * Note: As the `focus` event is executed synchronously when calling `input.focus()` + * AngularJS executes the expression using `scope.$evalAsync` if the event is fired + * during an `$apply` to ensure a consistent state. + * * @element window, input, select, textarea, a + * @priority 0 * @param {expression} ngFocus {@link guide/expression Expression} to evaluate upon - * focus. (Event object is available as `$event`) + * focus. ({@link guide/expression#-event- Event object is available as `$event`}) * * @example * See {@link ng.directive:ngClick ngClick} @@ -18134,14 +27676,23 @@ /** * @ngdoc directive - * @name ng.directive:ngBlur + * @name ngBlur * * @description * Specify custom behavior on blur event. * + * A [blur event](https://developer.mozilla.org/en-US/docs/Web/Events/blur) fires when + * an element has lost focus. + * + * Note: As the `blur` event is executed synchronously also during DOM manipulations + * (e.g. removing a focussed input), + * AngularJS executes the expression using `scope.$evalAsync` if the event is fired + * during an `$apply` to ensure a consistent state. + * * @element window, input, select, textarea, a + * @priority 0 * @param {expression} ngBlur {@link guide/expression Expression} to evaluate upon - * blur. (Event object is available as `$event`) + * blur. ({@link guide/expression#-event- Event object is available as `$event`}) * * @example * See {@link ng.directive:ngClick ngClick} @@ -18149,68 +27700,72 @@ /** * @ngdoc directive - * @name ng.directive:ngCopy + * @name ngCopy * * @description * Specify custom behavior on copy event. * * @element window, input, select, textarea, a + * @priority 0 * @param {expression} ngCopy {@link guide/expression Expression} to evaluate upon - * copy. (Event object is available as `$event`) + * copy. ({@link guide/expression#-event- Event object is available as `$event`}) * * @example - - + + copied: {{copied}} - - + + */ /** * @ngdoc directive - * @name ng.directive:ngCut + * @name ngCut * * @description * Specify custom behavior on cut event. * * @element window, input, select, textarea, a + * @priority 0 * @param {expression} ngCut {@link guide/expression Expression} to evaluate upon - * cut. (Event object is available as `$event`) + * cut. ({@link guide/expression#-event- Event object is available as `$event`}) * * @example - - + + cut: {{cut}} - - + + */ /** * @ngdoc directive - * @name ng.directive:ngPaste + * @name ngPaste * * @description * Specify custom behavior on paste event. * * @element window, input, select, textarea, a + * @priority 0 * @param {expression} ngPaste {@link guide/expression Expression} to evaluate upon - * paste. (Event object is available as `$event`) + * paste. ({@link guide/expression#-event- Event object is available as `$event`}) * * @example - - + + pasted: {{paste}} - - + + */ /** * @ngdoc directive - * @name ng.directive:ngIf + * @name ngIf * @restrict A + * @multiElement * * @description * The `ngIf` directive removes or recreates a portion of the DOM tree based on an @@ -18226,7 +27781,7 @@ * Note that when an element is removed using `ngIf` its scope is destroyed and a new scope * is created when the element is restored. The scope created within `ngIf` inherits from * its parent scope using - * {@link https://github.com/angular/angular.js/wiki/The-Nuances-of-Scope-Prototypal-Inheritance prototypal inheritance}. + * [prototypal inheritance](https://github.com/angular/angular.js/wiki/Understanding-Scopes#javascript-prototypal-inheritance). * An important implication of this is if `ngModel` is used within `ngIf` to bind to * a javascript primitive defined in the parent scope. In this case any modifications made to the * variable within the child scope will override (hide) the value in the parent scope. @@ -18240,8 +27795,10 @@ * and `leave` effects. * * @animations - * enter - happens just after the ngIf contents change and a new DOM element is created and injected into the ngIf container - * leave - happens just before the ngIf contents are removed from the DOM + * | Animation | Occurs | + * |----------------------------------|-------------------------------------| + * | {@link ng.$animate#enter enter} | just after the `ngIf` contents change and a new DOM element is created and injected into the `ngIf` container | + * | {@link ng.$animate#leave leave} | just before the `ngIf` contents are removed from the DOM | * * @element ANY * @scope @@ -18251,12 +27808,12 @@ * element is added to the DOM tree. * * @example - + - Click me:
      +
      Show when checked: - I'm removed when the checkbox is unchecked. + This is removed when the checkbox is unchecked.
      @@ -18267,7 +27824,6 @@ } .animate-if.ng-enter, .animate-if.ng-leave { - -webkit-transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s; transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s; } @@ -18283,25 +27839,26 @@
      */ - var ngIfDirective = ['$animate', function($animate) { + var ngIfDirective = ['$animate', '$compile', function($animate, $compile) { return { + multiElement: true, transclude: 'element', priority: 600, terminal: true, restrict: 'A', $$tlb: true, - link: function ($scope, $element, $attr, ctrl, $transclude) { - var block, childScope; + link: function($scope, $element, $attr, ctrl, $transclude) { + var block, childScope, previousElements; $scope.$watch($attr.ngIf, function ngIfWatchAction(value) { - if (toBoolean(value)) { + if (value) { if (!childScope) { - childScope = $scope.$new(); - $transclude(childScope, function (clone) { - clone[clone.length++] = document.createComment(' end ngIf: ' + $attr.ngIf + ' '); + $transclude(function(clone, newScope) { + childScope = newScope; + clone[clone.length++] = $compile.$$createComment('end ngIf', $attr.ngIf); // Note: We only need the first/last node of the cloned nodes. // However, we need to keep the reference to the jqlite wrapper as it might be changed later - // by a directive with templateUrl when it's template arrives. + // by a directive with templateUrl when its template arrives. block = { clone: clone }; @@ -18309,14 +27866,19 @@ }); } } else { - + if (previousElements) { + previousElements.remove(); + previousElements = null; + } if (childScope) { childScope.$destroy(); childScope = null; } - if (block) { - $animate.leave(getBlockElements(block.clone)); + previousElements = getBlockNodes(block.clone); + $animate.leave(previousElements).done(function(response) { + if (response !== false) previousElements = null; + }); block = null; } } @@ -18327,29 +27889,31 @@ /** * @ngdoc directive - * @name ng.directive:ngInclude + * @name ngInclude * @restrict ECA * * @description * Fetches, compiles and includes an external HTML fragment. * * By default, the template URL is restricted to the same domain and protocol as the - * application document. This is done by calling {@link ng.$sce#methods_getTrustedResourceUrl - * $sce.getTrustedResourceUrl} on it. To load templates from other domains or protocols - * you may either {@link ng.$sceDelegateProvider#methods_resourceUrlWhitelist whitelist them} or - * {@link ng.$sce#methods_trustAsResourceUrl wrap them} as trusted values. Refer to Angular's {@link - * ng.$sce Strict Contextual Escaping}. + * application document. This is done by calling {@link $sce#getTrustedResourceUrl + * $sce.getTrustedResourceUrl} on it. To load templates from other domains or protocols + * you may either {@link ng.$sceDelegateProvider#resourceUrlWhitelist whitelist them} or + * {@link $sce#trustAsResourceUrl wrap them} as trusted values. Refer to Angular's {@link + * ng.$sce Strict Contextual Escaping}. * * In addition, the browser's - * {@link https://code.google.com/p/browsersec/wiki/Part2#Same-origin_policy_for_XMLHttpRequest - * Same Origin Policy} and {@link http://www.w3.org/TR/cors/ Cross-Origin Resource Sharing - * (CORS)} policy may further restrict whether the template is successfully loaded. + * [Same Origin Policy](https://code.google.com/p/browsersec/wiki/Part2#Same-origin_policy_for_XMLHttpRequest) + * and [Cross-Origin Resource Sharing (CORS)](http://www.w3.org/TR/cors/) + * policy may further restrict whether the template is successfully loaded. * For example, `ngInclude` won't work for cross-domain requests on all browsers and for `file://` * access on some browsers. * * @animations - * enter - animation is used to bring new content into the browser. - * leave - animation is used to animate existing content away. + * | Animation | Occurs | + * |----------------------------------|-------------------------------------| + * | {@link ng.$animate#enter enter} | when the expression changes, on the new include | + * | {@link ng.$animate#leave leave} | when the expression changes, on the old include | * * The enter and leave animation occur concurrently. * @@ -18357,24 +27921,30 @@ * @priority 400 * * @param {string} ngInclude|src angular expression evaluating to URL. If the source is a string constant, - * make sure you wrap it in quotes, e.g. `src="'myPartialTemplate.html'"`. + * make sure you wrap it in **single** quotes, e.g. `src="'myPartialTemplate.html'"`. * @param {string=} onload Expression to evaluate when a new partial is loaded. + *
      + * **Note:** When using onload on SVG elements in IE11, the browser will try to call + * a function with the name on the window element, which will usually throw a + * "function is undefined" error. To fix this, you can instead use `data-onload` or a + * different form that {@link guide/directive#normalization matches} `onload`. + *
      * * @param {string=} autoscroll Whether `ngInclude` should call {@link ng.$anchorScroll - * $anchorScroll} to scroll the viewport after the content is loaded. + * $anchorScroll} to scroll the viewport after the content is loaded. * * - If the attribute is not set, disable scrolling. * - If the attribute is set without value, enable scrolling. * - Otherwise enable scrolling only if the expression evaluates to truthy value. * * @example - + -
      +
      - url of the template: {{template.url}} + url of the template: {{template.url}}
      @@ -18382,12 +27952,13 @@
      - function Ctrl($scope) { - $scope.templates = - [ { name: 'template1.html', url: 'template1.html'} - , { name: 'template2.html', url: 'template2.html'} ]; - $scope.template = $scope.templates[0]; - } + angular.module('includeExample', ['ngAnimate']) + .controller('ExampleController', ['$scope', function($scope) { + $scope.templates = + [{ name: 'template1.html', url: 'template1.html'}, + { name: 'template2.html', url: 'template2.html'}]; + $scope.template = $scope.templates[0]; + }]); Content of template1.html @@ -18409,7 +27980,6 @@ } .slide-animate.ng-enter, .slide-animate.ng-leave { - -webkit-transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s; transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s; position:absolute; @@ -18435,19 +28005,33 @@ top:50px; } - + + var templateSelect = element(by.model('template')); + var includeElem = element(by.css('[ng-include]')); + it('should load template1.html', function() { - expect(element('.doc-example-live [ng-include]').text()). - toMatch(/Content of template1.html/); + expect(includeElem.getText()).toMatch(/Content of template1.html/); }); + it('should load template2.html', function() { - select('template').option('1'); - expect(element('.doc-example-live [ng-include]').text()). - toMatch(/Content of template2.html/); + if (browser.params.browser === 'firefox') { + // Firefox can't handle using selects + // See https://github.com/angular/protractor/issues/480 + return; + } + templateSelect.click(); + templateSelect.all(by.css('option')).get(2).click(); + expect(includeElem.getText()).toMatch(/Content of template2.html/); }); + it('should change to blank', function() { - select('template').option(''); - expect(element('.doc-example-live [ng-include]')).toBe(undefined); + if (browser.params.browser === 'firefox') { + // Firefox can't handle using selects + return; + } + templateSelect.click(); + templateSelect.all(by.css('option')).get(0).click(); + expect(includeElem.isPresent()).toBe(false); }); @@ -18456,24 +28040,40 @@ /** * @ngdoc event - * @name ng.directive:ngInclude#$includeContentRequested - * @eventOf ng.directive:ngInclude + * @name ngInclude#$includeContentRequested * @eventType emit on the scope ngInclude was declared in * @description * Emitted every time the ngInclude content is requested. + * + * @param {Object} angularEvent Synthetic event object. + * @param {String} src URL of content to load. */ /** * @ngdoc event - * @name ng.directive:ngInclude#$includeContentLoaded - * @eventOf ng.directive:ngInclude + * @name ngInclude#$includeContentLoaded * @eventType emit on the current ngInclude scope * @description * Emitted every time the ngInclude content is reloaded. + * + * @param {Object} angularEvent Synthetic event object. + * @param {String} src URL of content to load. */ - var ngIncludeDirective = ['$http', '$templateCache', '$anchorScroll', '$animate', '$sce', - function($http, $templateCache, $anchorScroll, $animate, $sce) { + + + /** + * @ngdoc event + * @name ngInclude#$includeContentError + * @eventType emit on the scope ngInclude was declared in + * @description + * Emitted when a template HTTP request yields an erroneous response (status < 200 || status > 299) + * + * @param {Object} angularEvent Synthetic event object. + * @param {String} src URL of content to load. + */ + var ngIncludeDirective = ['$templateRequest', '$anchorScroll', '$animate', + function($templateRequest, $anchorScroll, $animate) { return { restrict: 'ECA', priority: 400, @@ -18482,35 +28082,48 @@ controller: angular.noop, compile: function(element, attr) { var srcExp = attr.ngInclude || attr.src, - onloadExp = attr.onload || '', - autoScrollExp = attr.autoscroll; + onloadExp = attr.onload || '', + autoScrollExp = attr.autoscroll; return function(scope, $element, $attr, ctrl, $transclude) { var changeCounter = 0, - currentScope, - currentElement; + currentScope, + previousElement, + currentElement; var cleanupLastIncludeContent = function() { + if (previousElement) { + previousElement.remove(); + previousElement = null; + } if (currentScope) { currentScope.$destroy(); currentScope = null; } - if(currentElement) { - $animate.leave(currentElement); + if (currentElement) { + $animate.leave(currentElement).done(function(response) { + if (response !== false) previousElement = null; + }); + previousElement = currentElement; currentElement = null; } }; - scope.$watch($sce.parseAsResourceUrl(srcExp), function ngIncludeWatchAction(src) { - var afterAnimation = function() { - if (isDefined(autoScrollExp) && (!autoScrollExp || scope.$eval(autoScrollExp))) { + scope.$watch(srcExp, function ngIncludeWatchAction(src) { + var afterAnimation = function(response) { + if (response !== false && isDefined(autoScrollExp) && + (!autoScrollExp || scope.$eval(autoScrollExp))) { $anchorScroll(); } }; var thisChangeId = ++changeCounter; if (src) { - $http.get(src, {cache: $templateCache}).success(function(response) { + //set the 2nd param to true to ignore the template request error so that the inner + //contents and scope can be cleaned up. + $templateRequest(src, true).then(function(response) { + if (scope.$$destroyed) return; + if (thisChangeId !== changeCounter) return; var newScope = scope.$new(); ctrl.template = response; @@ -18523,18 +28136,23 @@ // directives to non existing elements. var clone = $transclude(newScope, function(clone) { cleanupLastIncludeContent(); - $animate.enter(clone, null, $element, afterAnimation); + $animate.enter(clone, null, $element).done(afterAnimation); }); currentScope = newScope; currentElement = clone; - currentScope.$emit('$includeContentLoaded'); + currentScope.$emit('$includeContentLoaded', src); scope.$eval(onloadExp); - }).error(function() { - if (thisChangeId === changeCounter) cleanupLastIncludeContent(); - }); - scope.$emit('$includeContentRequested'); + }, function() { + if (scope.$$destroyed) return; + + if (thisChangeId === changeCounter) { + cleanupLastIncludeContent(); + scope.$emit('$includeContentError', src); + } + }); + scope.$emit('$includeContentRequested', src); } else { cleanupLastIncludeContent(); ctrl.template = null; @@ -18557,6 +28175,18 @@ priority: -400, require: 'ngInclude', link: function(scope, $element, $attr, ctrl) { + if (toString.call($element[0]).match(/SVG/)) { + // WebKit: https://bugs.webkit.org/show_bug.cgi?id=135698 --- SVG elements do not + // support innerHTML, so detect this here and try to generate the contents + // specially. + $element.empty(); + $compile(jqLiteBuildFragment(ctrl.template, window.document).childNodes)(scope, + function namespaceAdaptedClone(clone) { + $element.append(clone); + }, {futureParentElement: $element}); + return; + } + $element.html(ctrl.template); $compile($element.contents())(scope); } @@ -18565,18 +28195,27 @@ /** * @ngdoc directive - * @name ng.directive:ngInit + * @name ngInit * @restrict AC * * @description * The `ngInit` directive allows you to evaluate an expression in the * current scope. * - *
      - * The only appropriate use of `ngInit` is for aliasing special properties of - * {@link ../api/ng.directive:ngRepeat `ngRepeat`}, as seen in the demo below. Besides this case, you - * should use {@link guide/controller controllers} rather than `ngInit` - * to initialize values on a scope. + *
      + * This directive can be abused to add unnecessary amounts of logic into your templates. + * There are only a few appropriate uses of `ngInit`, such as for aliasing special properties of + * {@link ng.directive:ngRepeat `ngRepeat`}, as seen in the demo below; and for injecting data via + * server side scripting. Besides these few cases, you should use {@link guide/controller controllers} + * rather than `ngInit` to initialize values on a scope. + *
      + * + *
      + * **Note**: If you have assignment in `ngInit` along with a {@link ng.$filter `filter`}, make + * sure you have parentheses to ensure correct operator precedence: + *
      +   * `
      ` + *
      *
      * * @priority 450 @@ -18585,31 +28224,32 @@ * @param {expression} ngInit {@link guide/expression Expression} to eval. * * @example - - + + -
      +
      list[ {{outerIndex}} ][ {{innerIndex}} ] = {{value}};
      - - + + it('should alias index positions', function() { - expect(element('.example-init').text()) - .toBe('list[ 0 ][ 0 ] = a;' + - 'list[ 0 ][ 1 ] = b;' + - 'list[ 1 ][ 0 ] = c;' + - 'list[ 1 ][ 1 ] = d;'); + var elements = element.all(by.css('.example-init')); + expect(elements.get(0).getText()).toBe('list[ 0 ][ 0 ] = a;'); + expect(elements.get(1).getText()).toBe('list[ 0 ][ 1 ] = b;'); + expect(elements.get(2).getText()).toBe('list[ 1 ][ 0 ] = c;'); + expect(elements.get(3).getText()).toBe('list[ 1 ][ 1 ] = d;'); }); - - + + */ var ngInitDirective = ngDirective({ priority: 450, @@ -18624,7 +28264,1709 @@ /** * @ngdoc directive - * @name ng.directive:ngNonBindable + * @name ngList + * + * @description + * Text input that converts between a delimited string and an array of strings. The default + * delimiter is a comma followed by a space - equivalent to `ng-list=", "`. You can specify a custom + * delimiter as the value of the `ngList` attribute - for example, `ng-list=" | "`. + * + * The behaviour of the directive is affected by the use of the `ngTrim` attribute. + * * If `ngTrim` is set to `"false"` then whitespace around both the separator and each + * list item is respected. This implies that the user of the directive is responsible for + * dealing with whitespace but also allows you to use whitespace as a delimiter, such as a + * tab or newline character. + * * Otherwise whitespace around the delimiter is ignored when splitting (although it is respected + * when joining the list items back together) and whitespace around each list item is stripped + * before it is added to the model. + * + * ### Example with Validation + * + * + * + * angular.module('listExample', []) + * .controller('ExampleController', ['$scope', function($scope) { + * $scope.names = ['morpheus', 'neo', 'trinity']; + * }]); + * + * + *
      + * + * + * + * Required! + * + *
      + * names = {{names}}
      + * myForm.namesInput.$valid = {{myForm.namesInput.$valid}}
      + * myForm.namesInput.$error = {{myForm.namesInput.$error}}
      + * myForm.$valid = {{myForm.$valid}}
      + * myForm.$error.required = {{!!myForm.$error.required}}
      + *
      + *
      + * + * var listInput = element(by.model('names')); + * var names = element(by.exactBinding('names')); + * var valid = element(by.binding('myForm.namesInput.$valid')); + * var error = element(by.css('span.error')); + * + * it('should initialize to model', function() { + * expect(names.getText()).toContain('["morpheus","neo","trinity"]'); + * expect(valid.getText()).toContain('true'); + * expect(error.getCssValue('display')).toBe('none'); + * }); + * + * it('should be invalid if empty', function() { + * listInput.clear(); + * listInput.sendKeys(''); + * + * expect(names.getText()).toContain(''); + * expect(valid.getText()).toContain('false'); + * expect(error.getCssValue('display')).not.toBe('none'); + * }); + * + *
      + * + * ### Example - splitting on newline + * + * + * + *
      {{ list | json }}
      + *
      + * + * it("should split the text by newlines", function() { + * var listInput = element(by.model('list')); + * var output = element(by.binding('list | json')); + * listInput.sendKeys('abc\ndef\nghi'); + * expect(output.getText()).toContain('[\n "abc",\n "def",\n "ghi"\n]'); + * }); + * + *
      + * + * @element input + * @param {string=} ngList optional delimiter that should be used to split the value. + */ + var ngListDirective = function() { + return { + restrict: 'A', + priority: 100, + require: 'ngModel', + link: function(scope, element, attr, ctrl) { + var ngList = attr.ngList || ', '; + var trimValues = attr.ngTrim !== 'false'; + var separator = trimValues ? trim(ngList) : ngList; + + var parse = function(viewValue) { + // If the viewValue is invalid (say required but empty) it will be `undefined` + if (isUndefined(viewValue)) return; + + var list = []; + + if (viewValue) { + forEach(viewValue.split(separator), function(value) { + if (value) list.push(trimValues ? trim(value) : value); + }); + } + + return list; + }; + + ctrl.$parsers.push(parse); + ctrl.$formatters.push(function(value) { + if (isArray(value)) { + return value.join(ngList); + } + + return undefined; + }); + + // Override the standard $isEmpty because an empty array means the input is empty. + ctrl.$isEmpty = function(value) { + return !value || !value.length; + }; + } + }; + }; + + /* global VALID_CLASS: true, + INVALID_CLASS: true, + PRISTINE_CLASS: true, + DIRTY_CLASS: true, + UNTOUCHED_CLASS: true, + TOUCHED_CLASS: true, + PENDING_CLASS: true, + addSetValidityMethod: true, + setupValidity: true, + defaultModelOptions: false +*/ + + + var VALID_CLASS = 'ng-valid', + INVALID_CLASS = 'ng-invalid', + PRISTINE_CLASS = 'ng-pristine', + DIRTY_CLASS = 'ng-dirty', + UNTOUCHED_CLASS = 'ng-untouched', + TOUCHED_CLASS = 'ng-touched', + EMPTY_CLASS = 'ng-empty', + NOT_EMPTY_CLASS = 'ng-not-empty'; + + var ngModelMinErr = minErr('ngModel'); + + /** + * @ngdoc type + * @name ngModel.NgModelController + * + * @property {*} $viewValue The actual value from the control's view. For `input` elements, this is a + * String. See {@link ngModel.NgModelController#$setViewValue} for information about when the $viewValue + * is set. + * + * @property {*} $modelValue The value in the model that the control is bound to. + * + * @property {Array.} $parsers Array of functions to execute, as a pipeline, whenever + * the control updates the ngModelController with a new {@link ngModel.NgModelController#$viewValue + `$viewValue`} from the DOM, usually via user input. + See {@link ngModel.NgModelController#$setViewValue `$setViewValue()`} for a detailed lifecycle explanation. + Note that the `$parsers` are not called when the bound ngModel expression changes programmatically. + + The functions are called in array order, each passing + its return value through to the next. The last return value is forwarded to the + {@link ngModel.NgModelController#$validators `$validators`} collection. + + Parsers are used to sanitize / convert the {@link ngModel.NgModelController#$viewValue + `$viewValue`}. + + Returning `undefined` from a parser means a parse error occurred. In that case, + no {@link ngModel.NgModelController#$validators `$validators`} will run and the `ngModel` + will be set to `undefined` unless {@link ngModelOptions `ngModelOptions.allowInvalid`} + is set to `true`. The parse error is stored in `ngModel.$error.parse`. + + This simple example shows a parser that would convert text input value to lowercase: + * ```js + * function parse(value) { + * if (value) { + * return value.toLowerCase(); + * } + * } + * ngModelController.$parsers.push(parse); + * ``` + + * + * @property {Array.} $formatters Array of functions to execute, as a pipeline, whenever + the bound ngModel expression changes programmatically. The `$formatters` are not called when the + value of the control is changed by user interaction. + + Formatters are used to format / convert the {@link ngModel.NgModelController#$modelValue + `$modelValue`} for display in the control. + + The functions are called in reverse array order, each passing the value through to the + next. The last return value is used as the actual DOM value. + + This simple example shows a formatter that would convert the model value to uppercase: + + * ```js + * function format(value) { + * if (value) { + * return value.toUpperCase(); + * } + * } + * ngModel.$formatters.push(format); + * ``` + * + * @property {Object.} $validators A collection of validators that are applied + * whenever the model value changes. The key value within the object refers to the name of the + * validator while the function refers to the validation operation. The validation operation is + * provided with the model value as an argument and must return a true or false value depending + * on the response of that validation. + * + * ```js + * ngModel.$validators.validCharacters = function(modelValue, viewValue) { + * var value = modelValue || viewValue; + * return /[0-9]+/.test(value) && + * /[a-z]+/.test(value) && + * /[A-Z]+/.test(value) && + * /\W+/.test(value); + * }; + * ``` + * + * @property {Object.} $asyncValidators A collection of validations that are expected to + * perform an asynchronous validation (e.g. a HTTP request). The validation function that is provided + * is expected to return a promise when it is run during the model validation process. Once the promise + * is delivered then the validation status will be set to true when fulfilled and false when rejected. + * When the asynchronous validators are triggered, each of the validators will run in parallel and the model + * value will only be updated once all validators have been fulfilled. As long as an asynchronous validator + * is unfulfilled, its key will be added to the controllers `$pending` property. Also, all asynchronous validators + * will only run once all synchronous validators have passed. + * + * Please note that if $http is used then it is important that the server returns a success HTTP response code + * in order to fulfill the validation and a status level of `4xx` in order to reject the validation. + * + * ```js + * ngModel.$asyncValidators.uniqueUsername = function(modelValue, viewValue) { + * var value = modelValue || viewValue; + * + * // Lookup user by username + * return $http.get('/api/users/' + value). + * then(function resolved() { + * //username exists, this means validation fails + * return $q.reject('exists'); + * }, function rejected() { + * //username does not exist, therefore this validation passes + * return true; + * }); + * }; + * ``` + * + * @property {Array.} $viewChangeListeners Array of functions to execute whenever the + * view value has changed. It is called with no arguments, and its return value is ignored. + * This can be used in place of additional $watches against the model value. + * + * @property {Object} $error An object hash with all failing validator ids as keys. + * @property {Object} $pending An object hash with all pending validator ids as keys. + * + * @property {boolean} $untouched True if control has not lost focus yet. + * @property {boolean} $touched True if control has lost focus. + * @property {boolean} $pristine True if user has not interacted with the control yet. + * @property {boolean} $dirty True if user has already interacted with the control. + * @property {boolean} $valid True if there is no error. + * @property {boolean} $invalid True if at least one error on the control. + * @property {string} $name The name attribute of the control. + * + * @description + * + * `NgModelController` provides API for the {@link ngModel `ngModel`} directive. + * The controller contains services for data-binding, validation, CSS updates, and value formatting + * and parsing. It purposefully does not contain any logic which deals with DOM rendering or + * listening to DOM events. + * Such DOM related logic should be provided by other directives which make use of + * `NgModelController` for data-binding to control elements. + * Angular provides this DOM logic for most {@link input `input`} elements. + * At the end of this page you can find a {@link ngModel.NgModelController#custom-control-example + * custom control example} that uses `ngModelController` to bind to `contenteditable` elements. + * + * @example + * ### Custom Control Example + * This example shows how to use `NgModelController` with a custom control to achieve + * data-binding. Notice how different directives (`contenteditable`, `ng-model`, and `required`) + * collaborate together to achieve the desired result. + * + * `contenteditable` is an HTML5 attribute, which tells the browser to let the element + * contents be edited in place by the user. + * + * We are using the {@link ng.service:$sce $sce} service here and include the {@link ngSanitize $sanitize} + * module to automatically remove "bad" content like inline event listener (e.g. ``). + * However, as we are using `$sce` the model can still decide to provide unsafe content if it marks + * that content using the `$sce` service. + * + * + + [contenteditable] { + border: 1px solid black; + background-color: white; + min-height: 20px; + } + + .ng-invalid { + border: 1px solid red; + } + + + + angular.module('customControl', ['ngSanitize']). + directive('contenteditable', ['$sce', function($sce) { + return { + restrict: 'A', // only activate on element attribute + require: '?ngModel', // get a hold of NgModelController + link: function(scope, element, attrs, ngModel) { + if (!ngModel) return; // do nothing if no ng-model + + // Specify how UI should be updated + ngModel.$render = function() { + element.html($sce.getTrustedHtml(ngModel.$viewValue || '')); + }; + + // Listen for change events to enable binding + element.on('blur keyup change', function() { + scope.$evalAsync(read); + }); + read(); // initialize + + // Write data to the model + function read() { + var html = element.html(); + // When we clear the content editable the browser leaves a
      behind + // If strip-br attribute is provided then we strip this out + if (attrs.stripBr && html === '
      ') { + html = ''; + } + ngModel.$setViewValue(html); + } + } + }; + }]); +
      + +
      +
      Change me!
      + Required! +
      + +
      +
      + + it('should data-bind and become invalid', function() { + if (browser.params.browser === 'safari' || browser.params.browser === 'firefox') { + // SafariDriver can't handle contenteditable + // and Firefox driver can't clear contenteditables very well + return; + } + var contentEditable = element(by.css('[contenteditable]')); + var content = 'Change me!'; + + expect(contentEditable.getText()).toEqual(content); + + contentEditable.clear(); + contentEditable.sendKeys(protractor.Key.BACK_SPACE); + expect(contentEditable.getText()).toEqual(''); + expect(contentEditable.getAttribute('class')).toMatch(/ng-invalid-required/); + }); + + *
      + * + * + */ + NgModelController.$inject = ['$scope', '$exceptionHandler', '$attrs', '$element', '$parse', '$animate', '$timeout', '$q', '$interpolate']; + function NgModelController($scope, $exceptionHandler, $attr, $element, $parse, $animate, $timeout, $q, $interpolate) { + this.$viewValue = Number.NaN; + this.$modelValue = Number.NaN; + this.$$rawModelValue = undefined; // stores the parsed modelValue / model set from scope regardless of validity. + this.$validators = {}; + this.$asyncValidators = {}; + this.$parsers = []; + this.$formatters = []; + this.$viewChangeListeners = []; + this.$untouched = true; + this.$touched = false; + this.$pristine = true; + this.$dirty = false; + this.$valid = true; + this.$invalid = false; + this.$error = {}; // keep invalid keys here + this.$$success = {}; // keep valid keys here + this.$pending = undefined; // keep pending keys here + this.$name = $interpolate($attr.name || '', false)($scope); + this.$$parentForm = nullFormCtrl; + this.$options = defaultModelOptions; + + this.$$parsedNgModel = $parse($attr.ngModel); + this.$$parsedNgModelAssign = this.$$parsedNgModel.assign; + this.$$ngModelGet = this.$$parsedNgModel; + this.$$ngModelSet = this.$$parsedNgModelAssign; + this.$$pendingDebounce = null; + this.$$parserValid = undefined; + + this.$$currentValidationRunId = 0; + + // https://github.com/angular/angular.js/issues/15833 + // Prevent `$$scope` from being iterated over by `copy` when NgModelController is deep watched + Object.defineProperty(this, '$$scope', {value: $scope}); + this.$$attr = $attr; + this.$$element = $element; + this.$$animate = $animate; + this.$$timeout = $timeout; + this.$$parse = $parse; + this.$$q = $q; + this.$$exceptionHandler = $exceptionHandler; + + setupValidity(this); + setupModelWatcher(this); + } + + NgModelController.prototype = { + $$initGetterSetters: function() { + if (this.$options.getOption('getterSetter')) { + var invokeModelGetter = this.$$parse(this.$$attr.ngModel + '()'), + invokeModelSetter = this.$$parse(this.$$attr.ngModel + '($$$p)'); + + this.$$ngModelGet = function($scope) { + var modelValue = this.$$parsedNgModel($scope); + if (isFunction(modelValue)) { + modelValue = invokeModelGetter($scope); + } + return modelValue; + }; + this.$$ngModelSet = function($scope, newValue) { + if (isFunction(this.$$parsedNgModel($scope))) { + invokeModelSetter($scope, {$$$p: newValue}); + } else { + this.$$parsedNgModelAssign($scope, newValue); + } + }; + } else if (!this.$$parsedNgModel.assign) { + throw ngModelMinErr('nonassign', 'Expression \'{0}\' is non-assignable. Element: {1}', + this.$$attr.ngModel, startingTag(this.$$element)); + } + }, + + + /** + * @ngdoc method + * @name ngModel.NgModelController#$render + * + * @description + * Called when the view needs to be updated. It is expected that the user of the ng-model + * directive will implement this method. + * + * The `$render()` method is invoked in the following situations: + * + * * `$rollbackViewValue()` is called. If we are rolling back the view value to the last + * committed value then `$render()` is called to update the input control. + * * The value referenced by `ng-model` is changed programmatically and both the `$modelValue` and + * the `$viewValue` are different from last time. + * + * Since `ng-model` does not do a deep watch, `$render()` is only invoked if the values of + * `$modelValue` and `$viewValue` are actually different from their previous values. If `$modelValue` + * or `$viewValue` are objects (rather than a string or number) then `$render()` will not be + * invoked if you only change a property on the objects. + */ + $render: noop, + + /** + * @ngdoc method + * @name ngModel.NgModelController#$isEmpty + * + * @description + * This is called when we need to determine if the value of an input is empty. + * + * For instance, the required directive does this to work out if the input has data or not. + * + * The default `$isEmpty` function checks whether the value is `undefined`, `''`, `null` or `NaN`. + * + * You can override this for input directives whose concept of being empty is different from the + * default. The `checkboxInputType` directive does this because in its case a value of `false` + * implies empty. + * + * @param {*} value The value of the input to check for emptiness. + * @returns {boolean} True if `value` is "empty". + */ + $isEmpty: function(value) { + // eslint-disable-next-line no-self-compare + return isUndefined(value) || value === '' || value === null || value !== value; + }, + + $$updateEmptyClasses: function(value) { + if (this.$isEmpty(value)) { + this.$$animate.removeClass(this.$$element, NOT_EMPTY_CLASS); + this.$$animate.addClass(this.$$element, EMPTY_CLASS); + } else { + this.$$animate.removeClass(this.$$element, EMPTY_CLASS); + this.$$animate.addClass(this.$$element, NOT_EMPTY_CLASS); + } + }, + + /** + * @ngdoc method + * @name ngModel.NgModelController#$setPristine + * + * @description + * Sets the control to its pristine state. + * + * This method can be called to remove the `ng-dirty` class and set the control to its pristine + * state (`ng-pristine` class). A model is considered to be pristine when the control + * has not been changed from when first compiled. + */ + $setPristine: function() { + this.$dirty = false; + this.$pristine = true; + this.$$animate.removeClass(this.$$element, DIRTY_CLASS); + this.$$animate.addClass(this.$$element, PRISTINE_CLASS); + }, + + /** + * @ngdoc method + * @name ngModel.NgModelController#$setDirty + * + * @description + * Sets the control to its dirty state. + * + * This method can be called to remove the `ng-pristine` class and set the control to its dirty + * state (`ng-dirty` class). A model is considered to be dirty when the control has been changed + * from when first compiled. + */ + $setDirty: function() { + this.$dirty = true; + this.$pristine = false; + this.$$animate.removeClass(this.$$element, PRISTINE_CLASS); + this.$$animate.addClass(this.$$element, DIRTY_CLASS); + this.$$parentForm.$setDirty(); + }, + + /** + * @ngdoc method + * @name ngModel.NgModelController#$setUntouched + * + * @description + * Sets the control to its untouched state. + * + * This method can be called to remove the `ng-touched` class and set the control to its + * untouched state (`ng-untouched` class). Upon compilation, a model is set as untouched + * by default, however this function can be used to restore that state if the model has + * already been touched by the user. + */ + $setUntouched: function() { + this.$touched = false; + this.$untouched = true; + this.$$animate.setClass(this.$$element, UNTOUCHED_CLASS, TOUCHED_CLASS); + }, + + /** + * @ngdoc method + * @name ngModel.NgModelController#$setTouched + * + * @description + * Sets the control to its touched state. + * + * This method can be called to remove the `ng-untouched` class and set the control to its + * touched state (`ng-touched` class). A model is considered to be touched when the user has + * first focused the control element and then shifted focus away from the control (blur event). + */ + $setTouched: function() { + this.$touched = true; + this.$untouched = false; + this.$$animate.setClass(this.$$element, TOUCHED_CLASS, UNTOUCHED_CLASS); + }, + + /** + * @ngdoc method + * @name ngModel.NgModelController#$rollbackViewValue + * + * @description + * Cancel an update and reset the input element's value to prevent an update to the `$modelValue`, + * which may be caused by a pending debounced event or because the input is waiting for some + * future event. + * + * If you have an input that uses `ng-model-options` to set up debounced updates or updates that + * depend on special events such as `blur`, there can be a period when the `$viewValue` is out of + * sync with the ngModel's `$modelValue`. + * + * In this case, you can use `$rollbackViewValue()` to manually cancel the debounced / future update + * and reset the input to the last committed view value. + * + * It is also possible that you run into difficulties if you try to update the ngModel's `$modelValue` + * programmatically before these debounced/future events have resolved/occurred, because Angular's + * dirty checking mechanism is not able to tell whether the model has actually changed or not. + * + * The `$rollbackViewValue()` method should be called before programmatically changing the model of an + * input which may have such events pending. This is important in order to make sure that the + * input field will be updated with the new model value and any pending operations are cancelled. + * + * + * + * angular.module('cancel-update-example', []) + * + * .controller('CancelUpdateController', ['$scope', function($scope) { + * $scope.model = {value1: '', value2: ''}; + * + * $scope.setEmpty = function(e, value, rollback) { + * if (e.keyCode === 27) { + * e.preventDefault(); + * if (rollback) { + * $scope.myForm[value].$rollbackViewValue(); + * } + * $scope.model[value] = ''; + * } + * }; + * }]); + * + * + *
      + *

      Both of these inputs are only updated if they are blurred. Hitting escape should + * empty them. Follow these steps and observe the difference:

      + *
        + *
      1. Type something in the input. You will see that the model is not yet updated
      2. + *
      3. Press the Escape key. + *
          + *
        1. In the first example, nothing happens, because the model is already '', and no + * update is detected. If you blur the input, the model will be set to the current view. + *
        2. + *
        3. In the second example, the pending update is cancelled, and the input is set back + * to the last committed view value (''). Blurring the input does nothing. + *
        4. + *
        + *
      4. + *
      + * + *
      + *
      + *

      Without $rollbackViewValue():

      + * + * value1: "{{ model.value1 }}" + *
      + * + *
      + *

      With $rollbackViewValue():

      + * + * value2: "{{ model.value2 }}" + *
      + *
      + *
      + *
      + + div { + display: table-cell; + } + div:nth-child(1) { + padding-right: 30px; + } + + + *
      + */ + $rollbackViewValue: function() { + this.$$timeout.cancel(this.$$pendingDebounce); + this.$viewValue = this.$$lastCommittedViewValue; + this.$render(); + }, + + /** + * @ngdoc method + * @name ngModel.NgModelController#$validate + * + * @description + * Runs each of the registered validators (first synchronous validators and then + * asynchronous validators). + * If the validity changes to invalid, the model will be set to `undefined`, + * unless {@link ngModelOptions `ngModelOptions.allowInvalid`} is `true`. + * If the validity changes to valid, it will set the model to the last available valid + * `$modelValue`, i.e. either the last parsed value or the last value set from the scope. + */ + $validate: function() { + // ignore $validate before model is initialized + if (isNumberNaN(this.$modelValue)) { + return; + } + + var viewValue = this.$$lastCommittedViewValue; + // Note: we use the $$rawModelValue as $modelValue might have been + // set to undefined during a view -> model update that found validation + // errors. We can't parse the view here, since that could change + // the model although neither viewValue nor the model on the scope changed + var modelValue = this.$$rawModelValue; + + var prevValid = this.$valid; + var prevModelValue = this.$modelValue; + + var allowInvalid = this.$options.getOption('allowInvalid'); + + var that = this; + this.$$runValidators(modelValue, viewValue, function(allValid) { + // If there was no change in validity, don't update the model + // This prevents changing an invalid modelValue to undefined + if (!allowInvalid && prevValid !== allValid) { + // Note: Don't check this.$valid here, as we could have + // external validators (e.g. calculated on the server), + // that just call $setValidity and need the model value + // to calculate their validity. + that.$modelValue = allValid ? modelValue : undefined; + + if (that.$modelValue !== prevModelValue) { + that.$$writeModelToScope(); + } + } + }); + }, + + $$runValidators: function(modelValue, viewValue, doneCallback) { + this.$$currentValidationRunId++; + var localValidationRunId = this.$$currentValidationRunId; + var that = this; + + // check parser error + if (!processParseErrors()) { + validationDone(false); + return; + } + if (!processSyncValidators()) { + validationDone(false); + return; + } + processAsyncValidators(); + + function processParseErrors() { + var errorKey = that.$$parserName || 'parse'; + if (isUndefined(that.$$parserValid)) { + setValidity(errorKey, null); + } else { + if (!that.$$parserValid) { + forEach(that.$validators, function(v, name) { + setValidity(name, null); + }); + forEach(that.$asyncValidators, function(v, name) { + setValidity(name, null); + }); + } + // Set the parse error last, to prevent unsetting it, should a $validators key == parserName + setValidity(errorKey, that.$$parserValid); + return that.$$parserValid; + } + return true; + } + + function processSyncValidators() { + var syncValidatorsValid = true; + forEach(that.$validators, function(validator, name) { + var result = Boolean(validator(modelValue, viewValue)); + syncValidatorsValid = syncValidatorsValid && result; + setValidity(name, result); + }); + if (!syncValidatorsValid) { + forEach(that.$asyncValidators, function(v, name) { + setValidity(name, null); + }); + return false; + } + return true; + } + + function processAsyncValidators() { + var validatorPromises = []; + var allValid = true; + forEach(that.$asyncValidators, function(validator, name) { + var promise = validator(modelValue, viewValue); + if (!isPromiseLike(promise)) { + throw ngModelMinErr('nopromise', + 'Expected asynchronous validator to return a promise but got \'{0}\' instead.', promise); + } + setValidity(name, undefined); + validatorPromises.push(promise.then(function() { + setValidity(name, true); + }, function() { + allValid = false; + setValidity(name, false); + })); + }); + if (!validatorPromises.length) { + validationDone(true); + } else { + that.$$q.all(validatorPromises).then(function() { + validationDone(allValid); + }, noop); + } + } + + function setValidity(name, isValid) { + if (localValidationRunId === that.$$currentValidationRunId) { + that.$setValidity(name, isValid); + } + } + + function validationDone(allValid) { + if (localValidationRunId === that.$$currentValidationRunId) { + + doneCallback(allValid); + } + } + }, + + /** + * @ngdoc method + * @name ngModel.NgModelController#$commitViewValue + * + * @description + * Commit a pending update to the `$modelValue`. + * + * Updates may be pending by a debounced event or because the input is waiting for a some future + * event defined in `ng-model-options`. this method is rarely needed as `NgModelController` + * usually handles calling this in response to input events. + */ + $commitViewValue: function() { + var viewValue = this.$viewValue; + + this.$$timeout.cancel(this.$$pendingDebounce); + + // If the view value has not changed then we should just exit, except in the case where there is + // a native validator on the element. In this case the validation state may have changed even though + // the viewValue has stayed empty. + if (this.$$lastCommittedViewValue === viewValue && (viewValue !== '' || !this.$$hasNativeValidators)) { + return; + } + this.$$updateEmptyClasses(viewValue); + this.$$lastCommittedViewValue = viewValue; + + // change to dirty + if (this.$pristine) { + this.$setDirty(); + } + this.$$parseAndValidate(); + }, + + $$parseAndValidate: function() { + var viewValue = this.$$lastCommittedViewValue; + var modelValue = viewValue; + var that = this; + + this.$$parserValid = isUndefined(modelValue) ? undefined : true; + + if (this.$$parserValid) { + for (var i = 0; i < this.$parsers.length; i++) { + modelValue = this.$parsers[i](modelValue); + if (isUndefined(modelValue)) { + this.$$parserValid = false; + break; + } + } + } + if (isNumberNaN(this.$modelValue)) { + // this.$modelValue has not been touched yet... + this.$modelValue = this.$$ngModelGet(this.$$scope); + } + var prevModelValue = this.$modelValue; + var allowInvalid = this.$options.getOption('allowInvalid'); + this.$$rawModelValue = modelValue; + + if (allowInvalid) { + this.$modelValue = modelValue; + writeToModelIfNeeded(); + } + + // Pass the $$lastCommittedViewValue here, because the cached viewValue might be out of date. + // This can happen if e.g. $setViewValue is called from inside a parser + this.$$runValidators(modelValue, this.$$lastCommittedViewValue, function(allValid) { + if (!allowInvalid) { + // Note: Don't check this.$valid here, as we could have + // external validators (e.g. calculated on the server), + // that just call $setValidity and need the model value + // to calculate their validity. + that.$modelValue = allValid ? modelValue : undefined; + writeToModelIfNeeded(); + } + }); + + function writeToModelIfNeeded() { + if (that.$modelValue !== prevModelValue) { + that.$$writeModelToScope(); + } + } + }, + + $$writeModelToScope: function() { + this.$$ngModelSet(this.$$scope, this.$modelValue); + forEach(this.$viewChangeListeners, function(listener) { + try { + listener(); + } catch (e) { + // eslint-disable-next-line no-invalid-this + this.$$exceptionHandler(e); + } + }, this); + }, + + /** + * @ngdoc method + * @name ngModel.NgModelController#$setViewValue + * + * @description + * Update the view value. + * + * This method should be called when a control wants to change the view value; typically, + * this is done from within a DOM event handler. For example, the {@link ng.directive:input input} + * directive calls it when the value of the input changes and {@link ng.directive:select select} + * calls it when an option is selected. + * + * When `$setViewValue` is called, the new `value` will be staged for committing through the `$parsers` + * and `$validators` pipelines. If there are no special {@link ngModelOptions} specified then the staged + * value is sent directly for processing through the `$parsers` pipeline. After this, the `$validators` and + * `$asyncValidators` are called and the value is applied to `$modelValue`. + * Finally, the value is set to the **expression** specified in the `ng-model` attribute and + * all the registered change listeners, in the `$viewChangeListeners` list are called. + * + * In case the {@link ng.directive:ngModelOptions ngModelOptions} directive is used with `updateOn` + * and the `default` trigger is not listed, all those actions will remain pending until one of the + * `updateOn` events is triggered on the DOM element. + * All these actions will be debounced if the {@link ng.directive:ngModelOptions ngModelOptions} + * directive is used with a custom debounce for this particular event. + * Note that a `$digest` is only triggered once the `updateOn` events are fired, or if `debounce` + * is specified, once the timer runs out. + * + * When used with standard inputs, the view value will always be a string (which is in some cases + * parsed into another type, such as a `Date` object for `input[date]`.) + * However, custom controls might also pass objects to this method. In this case, we should make + * a copy of the object before passing it to `$setViewValue`. This is because `ngModel` does not + * perform a deep watch of objects, it only looks for a change of identity. If you only change + * the property of the object then ngModel will not realize that the object has changed and + * will not invoke the `$parsers` and `$validators` pipelines. For this reason, you should + * not change properties of the copy once it has been passed to `$setViewValue`. + * Otherwise you may cause the model value on the scope to change incorrectly. + * + *
      + * In any case, the value passed to the method should always reflect the current value + * of the control. For example, if you are calling `$setViewValue` for an input element, + * you should pass the input DOM value. Otherwise, the control and the scope model become + * out of sync. It's also important to note that `$setViewValue` does not call `$render` or change + * the control's DOM value in any way. If we want to change the control's DOM value + * programmatically, we should update the `ngModel` scope expression. Its new value will be + * picked up by the model controller, which will run it through the `$formatters`, `$render` it + * to update the DOM, and finally call `$validate` on it. + *
      + * + * @param {*} value value from the view. + * @param {string} trigger Event that triggered the update. + */ + $setViewValue: function(value, trigger) { + this.$viewValue = value; + if (this.$options.getOption('updateOnDefault')) { + this.$$debounceViewValueCommit(trigger); + } + }, + + $$debounceViewValueCommit: function(trigger) { + var debounceDelay = this.$options.getOption('debounce'); + + if (isNumber(debounceDelay[trigger])) { + debounceDelay = debounceDelay[trigger]; + } else if (isNumber(debounceDelay['default'])) { + debounceDelay = debounceDelay['default']; + } + + this.$$timeout.cancel(this.$$pendingDebounce); + var that = this; + if (debounceDelay > 0) { // this fails if debounceDelay is an object + this.$$pendingDebounce = this.$$timeout(function() { + that.$commitViewValue(); + }, debounceDelay); + } else if (this.$$scope.$root.$$phase) { + this.$commitViewValue(); + } else { + this.$$scope.$apply(function() { + that.$commitViewValue(); + }); + } + }, + + /** + * @ngdoc method + * + * @name ngModel.NgModelController#$overrideModelOptions + * + * @description + * + * Override the current model options settings programmatically. + * + * The previous `ModelOptions` value will not be modified. Instead, a + * new `ModelOptions` object will inherit from the previous one overriding + * or inheriting settings that are defined in the given parameter. + * + * See {@link ngModelOptions} for information about what options can be specified + * and how model option inheritance works. + * + * @param {Object} options a hash of settings to override the previous options + * + */ + $overrideModelOptions: function(options) { + this.$options = this.$options.createChild(options); + } + }; + + function setupModelWatcher(ctrl) { + // model -> value + // Note: we cannot use a normal scope.$watch as we want to detect the following: + // 1. scope value is 'a' + // 2. user enters 'b' + // 3. ng-change kicks in and reverts scope value to 'a' + // -> scope value did not change since the last digest as + // ng-change executes in apply phase + // 4. view should be changed back to 'a' + ctrl.$$scope.$watch(function ngModelWatch(scope) { + var modelValue = ctrl.$$ngModelGet(scope); + + // if scope model value and ngModel value are out of sync + // TODO(perf): why not move this to the action fn? + if (modelValue !== ctrl.$modelValue && + // checks for NaN is needed to allow setting the model to NaN when there's an asyncValidator + // eslint-disable-next-line no-self-compare + (ctrl.$modelValue === ctrl.$modelValue || modelValue === modelValue) + ) { + ctrl.$modelValue = ctrl.$$rawModelValue = modelValue; + ctrl.$$parserValid = undefined; + + var formatters = ctrl.$formatters, + idx = formatters.length; + + var viewValue = modelValue; + while (idx--) { + viewValue = formatters[idx](viewValue); + } + if (ctrl.$viewValue !== viewValue) { + ctrl.$$updateEmptyClasses(viewValue); + ctrl.$viewValue = ctrl.$$lastCommittedViewValue = viewValue; + ctrl.$render(); + + // It is possible that model and view value have been updated during render + ctrl.$$runValidators(ctrl.$modelValue, ctrl.$viewValue, noop); + } + } + + return modelValue; + }); + } + + /** + * @ngdoc method + * @name ngModel.NgModelController#$setValidity + * + * @description + * Change the validity state, and notify the form. + * + * This method can be called within $parsers/$formatters or a custom validation implementation. + * However, in most cases it should be sufficient to use the `ngModel.$validators` and + * `ngModel.$asyncValidators` collections which will call `$setValidity` automatically. + * + * @param {string} validationErrorKey Name of the validator. The `validationErrorKey` will be assigned + * to either `$error[validationErrorKey]` or `$pending[validationErrorKey]` + * (for unfulfilled `$asyncValidators`), so that it is available for data-binding. + * The `validationErrorKey` should be in camelCase and will get converted into dash-case + * for class name. Example: `myError` will result in `ng-valid-my-error` and `ng-invalid-my-error` + * classes and can be bound to as `{{ someForm.someControl.$error.myError }}`. + * @param {boolean} isValid Whether the current state is valid (true), invalid (false), pending (undefined), + * or skipped (null). Pending is used for unfulfilled `$asyncValidators`. + * Skipped is used by Angular when validators do not run because of parse errors and + * when `$asyncValidators` do not run because any of the `$validators` failed. + */ + addSetValidityMethod({ + clazz: NgModelController, + set: function(object, property) { + object[property] = true; + }, + unset: function(object, property) { + delete object[property]; + } + }); + + + /** + * @ngdoc directive + * @name ngModel + * + * @element input + * @priority 1 + * + * @description + * The `ngModel` directive binds an `input`,`select`, `textarea` (or custom form control) to a + * property on the scope using {@link ngModel.NgModelController NgModelController}, + * which is created and exposed by this directive. + * + * `ngModel` is responsible for: + * + * - Binding the view into the model, which other directives such as `input`, `textarea` or `select` + * require. + * - Providing validation behavior (i.e. required, number, email, url). + * - Keeping the state of the control (valid/invalid, dirty/pristine, touched/untouched, validation errors). + * - Setting related css classes on the element (`ng-valid`, `ng-invalid`, `ng-dirty`, `ng-pristine`, `ng-touched`, + * `ng-untouched`, `ng-empty`, `ng-not-empty`) including animations. + * - Registering the control with its parent {@link ng.directive:form form}. + * + * Note: `ngModel` will try to bind to the property given by evaluating the expression on the + * current scope. If the property doesn't already exist on this scope, it will be created + * implicitly and added to the scope. + * + * For best practices on using `ngModel`, see: + * + * - [Understanding Scopes](https://github.com/angular/angular.js/wiki/Understanding-Scopes) + * + * For basic examples, how to use `ngModel`, see: + * + * - {@link ng.directive:input input} + * - {@link input[text] text} + * - {@link input[checkbox] checkbox} + * - {@link input[radio] radio} + * - {@link input[number] number} + * - {@link input[email] email} + * - {@link input[url] url} + * - {@link input[date] date} + * - {@link input[datetime-local] datetime-local} + * - {@link input[time] time} + * - {@link input[month] month} + * - {@link input[week] week} + * - {@link ng.directive:select select} + * - {@link ng.directive:textarea textarea} + * + * # Complex Models (objects or collections) + * + * By default, `ngModel` watches the model by reference, not value. This is important to know when + * binding inputs to models that are objects (e.g. `Date`) or collections (e.g. arrays). If only properties of the + * object or collection change, `ngModel` will not be notified and so the input will not be re-rendered. + * + * The model must be assigned an entirely new object or collection before a re-rendering will occur. + * + * Some directives have options that will cause them to use a custom `$watchCollection` on the model expression + * - for example, `ngOptions` will do so when a `track by` clause is included in the comprehension expression or + * if the select is given the `multiple` attribute. + * + * The `$watchCollection()` method only does a shallow comparison, meaning that changing properties deeper than the + * first level of the object (or only changing the properties of an item in the collection if it's an array) will still + * not trigger a re-rendering of the model. + * + * # CSS classes + * The following CSS classes are added and removed on the associated input/select/textarea element + * depending on the validity of the model. + * + * - `ng-valid`: the model is valid + * - `ng-invalid`: the model is invalid + * - `ng-valid-[key]`: for each valid key added by `$setValidity` + * - `ng-invalid-[key]`: for each invalid key added by `$setValidity` + * - `ng-pristine`: the control hasn't been interacted with yet + * - `ng-dirty`: the control has been interacted with + * - `ng-touched`: the control has been blurred + * - `ng-untouched`: the control hasn't been blurred + * - `ng-pending`: any `$asyncValidators` are unfulfilled + * - `ng-empty`: the view does not contain a value or the value is deemed "empty", as defined + * by the {@link ngModel.NgModelController#$isEmpty} method + * - `ng-not-empty`: the view contains a non-empty value + * + * Keep in mind that ngAnimate can detect each of these classes when added and removed. + * + * ## Animation Hooks + * + * Animations within models are triggered when any of the associated CSS classes are added and removed + * on the input element which is attached to the model. These classes include: `.ng-pristine`, `.ng-dirty`, + * `.ng-invalid` and `.ng-valid` as well as any other validations that are performed on the model itself. + * The animations that are triggered within ngModel are similar to how they work in ngClass and + * animations can be hooked into using CSS transitions, keyframes as well as JS animations. + * + * The following example shows a simple way to utilize CSS transitions to style an input element + * that has been rendered as invalid after it has been validated: + * + *
      +   * //be sure to include ngAnimate as a module to hook into more
      +   * //advanced animations
      +   * .my-input {
      + *   transition:0.5s linear all;
      + *   background: white;
      + * }
      +   * .my-input.ng-invalid {
      + *   background: red;
      + *   color:white;
      + * }
      +   * 
      + * + * @example + * + + + +

      + Update input to see transitions when valid/invalid. + Integer is a valid value. +

      +
      + +
      +
      + *
      + * + * ## Binding to a getter/setter + * + * Sometimes it's helpful to bind `ngModel` to a getter/setter function. A getter/setter is a + * function that returns a representation of the model when called with zero arguments, and sets + * the internal state of a model when called with an argument. It's sometimes useful to use this + * for models that have an internal representation that's different from what the model exposes + * to the view. + * + *
      + * **Best Practice:** It's best to keep getters fast because Angular is likely to call them more + * frequently than other parts of your code. + *
      + * + * You use this behavior by adding `ng-model-options="{ getterSetter: true }"` to an element that + * has `ng-model` attached to it. You can also add `ng-model-options="{ getterSetter: true }"` to + * a `
      `, which will enable this behavior for all ``s within it. See + * {@link ng.directive:ngModelOptions `ngModelOptions`} for more. + * + * The following example shows how to use `ngModel` with a getter/setter: + * + * @example + * + +
      + + + +
      user.name = 
      +
      +
      + + angular.module('getterSetterExample', []) + .controller('ExampleController', ['$scope', function($scope) { + var _name = 'Brian'; + $scope.user = { + name: function(newName) { + // Note that newName can be undefined for two reasons: + // 1. Because it is called as a getter and thus called with no arguments + // 2. Because the property should actually be set to undefined. This happens e.g. if the + // input is invalid + return arguments.length ? (_name = newName) : _name; + } + }; + }]); + + *
      + */ + var ngModelDirective = ['$rootScope', function($rootScope) { + return { + restrict: 'A', + require: ['ngModel', '^?form', '^?ngModelOptions'], + controller: NgModelController, + // Prelink needs to run before any input directive + // so that we can set the NgModelOptions in NgModelController + // before anyone else uses it. + priority: 1, + compile: function ngModelCompile(element) { + // Setup initial state of the control + element.addClass(PRISTINE_CLASS).addClass(UNTOUCHED_CLASS).addClass(VALID_CLASS); + + return { + pre: function ngModelPreLink(scope, element, attr, ctrls) { + var modelCtrl = ctrls[0], + formCtrl = ctrls[1] || modelCtrl.$$parentForm, + optionsCtrl = ctrls[2]; + + if (optionsCtrl) { + modelCtrl.$options = optionsCtrl.$options; + } + + modelCtrl.$$initGetterSetters(); + + // notify others, especially parent forms + formCtrl.$addControl(modelCtrl); + + attr.$observe('name', function(newValue) { + if (modelCtrl.$name !== newValue) { + modelCtrl.$$parentForm.$$renameControl(modelCtrl, newValue); + } + }); + + scope.$on('$destroy', function() { + modelCtrl.$$parentForm.$removeControl(modelCtrl); + }); + }, + post: function ngModelPostLink(scope, element, attr, ctrls) { + var modelCtrl = ctrls[0]; + if (modelCtrl.$options.getOption('updateOn')) { + element.on(modelCtrl.$options.getOption('updateOn'), function(ev) { + modelCtrl.$$debounceViewValueCommit(ev && ev.type); + }); + } + + function setTouched() { + modelCtrl.$setTouched(); + } + + element.on('blur', function() { + if (modelCtrl.$touched) return; + + if ($rootScope.$$phase) { + scope.$evalAsync(setTouched); + } else { + scope.$apply(setTouched); + } + }); + } + }; + } + }; + }]; + + /* exported defaultModelOptions */ + var defaultModelOptions; + var DEFAULT_REGEXP = /(\s+|^)default(\s+|$)/; + + /** + * @ngdoc type + * @name ModelOptions + * @description + * A container for the options set by the {@link ngModelOptions} directive + */ + function ModelOptions(options) { + this.$$options = options; + } + + ModelOptions.prototype = { + + /** + * @ngdoc method + * @name ModelOptions#getOption + * @param {string} name the name of the option to retrieve + * @returns {*} the value of the option + * @description + * Returns the value of the given option + */ + getOption: function(name) { + return this.$$options[name]; + }, + + /** + * @ngdoc method + * @name ModelOptions#createChild + * @param {Object} options a hash of options for the new child that will override the parent's options + * @return {ModelOptions} a new `ModelOptions` object initialized with the given options. + */ + createChild: function(options) { + var inheritAll = false; + + // make a shallow copy + options = extend({}, options); + + // Inherit options from the parent if specified by the value `"$inherit"` + forEach(options, /* @this */ function(option, key) { + if (option === '$inherit') { + if (key === '*') { + inheritAll = true; + } else { + options[key] = this.$$options[key]; + // `updateOn` is special so we must also inherit the `updateOnDefault` option + if (key === 'updateOn') { + options.updateOnDefault = this.$$options.updateOnDefault; + } + } + } else { + if (key === 'updateOn') { + // If the `updateOn` property contains the `default` event then we have to remove + // it from the event list and set the `updateOnDefault` flag. + options.updateOnDefault = false; + options[key] = trim(option.replace(DEFAULT_REGEXP, function() { + options.updateOnDefault = true; + return ' '; + })); + } + } + }, this); + + if (inheritAll) { + // We have a property of the form: `"*": "$inherit"` + delete options['*']; + defaults(options, this.$$options); + } + + // Finally add in any missing defaults + defaults(options, defaultModelOptions.$$options); + + return new ModelOptions(options); + } + }; + + + defaultModelOptions = new ModelOptions({ + updateOn: '', + updateOnDefault: true, + debounce: 0, + getterSetter: false, + allowInvalid: false, + timezone: null + }); + + + /** + * @ngdoc directive + * @name ngModelOptions + * + * @description + * This directive allows you to modify the behaviour of {@link ngModel} directives within your + * application. You can specify an `ngModelOptions` directive on any element. All {@link ngModel} + * directives will use the options of their nearest `ngModelOptions` ancestor. + * + * The `ngModelOptions` settings are found by evaluating the value of the attribute directive as + * an Angular expression. This expression should evaluate to an object, whose properties contain + * the settings. For example: `
      + *
      + * + *
      + *
      + * ``` + * + * the `input` element will have the following settings + * + * ```js + * { allowInvalid: true, updateOn: 'default', debounce: 0 } + * ``` + * + * Notice that the `debounce` setting was not inherited and used the default value instead. + * + * You can specify that all undefined settings are automatically inherited from an ancestor by + * including a property with key of `"*"` and value of `"$inherit"`. + * + * For example given the following fragment of HTML + * + * + * ```html + *
      + *
      + * + *
      + *
      + * ``` + * + * the `input` element will have the following settings + * + * ```js + * { allowInvalid: true, updateOn: 'default', debounce: 200 } + * ``` + * + * Notice that the `debounce` setting now inherits the value from the outer `
      ` element. + * + * If you are creating a reusable component then you should be careful when using `"*": "$inherit"` + * since you may inadvertently inherit a setting in the future that changes the behavior of your component. + * + * + * ## Triggering and debouncing model updates + * + * The `updateOn` and `debounce` properties allow you to specify a custom list of events that will + * trigger a model update and/or a debouncing delay so that the actual update only takes place when + * a timer expires; this timer will be reset after another change takes place. + * + * Given the nature of `ngModelOptions`, the value displayed inside input fields in the view might + * be different from the value in the actual model. This means that if you update the model you + * should also invoke {@link ngModel.NgModelController#$rollbackViewValue} on the relevant input field in + * order to make sure it is synchronized with the model and that any debounced action is canceled. + * + * The easiest way to reference the control's {@link ngModel.NgModelController#$rollbackViewValue} + * method is by making sure the input is placed inside a form that has a `name` attribute. This is + * important because `form` controllers are published to the related scope under the name in their + * `name` attribute. + * + * Any pending changes will take place immediately when an enclosing form is submitted via the + * `submit` event. Note that `ngClick` events will occur before the model is updated. Use `ngSubmit` + * to have access to the updated model. + * + * The following example shows how to override immediate updates. Changes on the inputs within the + * form will update the model only when the control loses focus (blur event). If `escape` key is + * pressed while the input field is focused, the value is reset to the value in the current model. + * + * + * + *
      + *
      + *
      + *
      + *
      + *
      user.name = 
      + *
      + *
      + * + * angular.module('optionsExample', []) + * .controller('ExampleController', ['$scope', function($scope) { + * $scope.user = { name: 'say', data: '' }; + * + * $scope.cancel = function(e) { + * if (e.keyCode === 27) { + * $scope.userForm.userName.$rollbackViewValue(); + * } + * }; + * }]); + * + * + * var model = element(by.binding('user.name')); + * var input = element(by.model('user.name')); + * var other = element(by.model('user.data')); + * + * it('should allow custom events', function() { + * input.sendKeys(' hello'); + * input.click(); + * expect(model.getText()).toEqual('say'); + * other.click(); + * expect(model.getText()).toEqual('say hello'); + * }); + * + * it('should $rollbackViewValue when model changes', function() { + * input.sendKeys(' hello'); + * expect(input.getAttribute('value')).toEqual('say hello'); + * input.sendKeys(protractor.Key.ESCAPE); + * expect(input.getAttribute('value')).toEqual('say'); + * other.click(); + * expect(model.getText()).toEqual('say'); + * }); + * + *
      + * + * The next example shows how to debounce model changes. Model will be updated only 1 sec after last change. + * If the `Clear` button is pressed, any debounced action is canceled and the value becomes empty. + * + * + * + *
      + *
      + * Name: + * + *
      + *
      + *
      user.name = 
      + *
      + *
      + * + * angular.module('optionsExample', []) + * .controller('ExampleController', ['$scope', function($scope) { + * $scope.user = { name: 'say' }; + * }]); + * + *
      + * + * ## Model updates and validation + * + * The default behaviour in `ngModel` is that the model value is set to `undefined` when the + * validation determines that the value is invalid. By setting the `allowInvalid` property to true, + * the model will still be updated even if the value is invalid. + * + * + * ## Connecting to the scope + * + * By setting the `getterSetter` property to true you are telling ngModel that the `ngModel` expression + * on the scope refers to a "getter/setter" function rather than the value itself. + * + * The following example shows how to bind to getter/setters: + * + * + * + *
      + *
      + * + *
      + *
      user.name = 
      + *
      + *
      + * + * angular.module('getterSetterExample', []) + * .controller('ExampleController', ['$scope', function($scope) { + * var _name = 'Brian'; + * $scope.user = { + * name: function(newName) { + * return angular.isDefined(newName) ? (_name = newName) : _name; + * } + * }; + * }]); + * + *
      + * + * + * ## Specifying timezones + * + * You can specify the timezone that date/time input directives expect by providing its name in the + * `timezone` property. + * + * @param {Object} ngModelOptions options to apply to {@link ngModel} directives on this element and + * and its descendents. Valid keys are: + * - `updateOn`: string specifying which event should the input be bound to. You can set several + * events using an space delimited list. There is a special event called `default` that + * matches the default events belonging to the control. + * - `debounce`: integer value which contains the debounce model update value in milliseconds. A + * value of 0 triggers an immediate update. If an object is supplied instead, you can specify a + * custom value for each event. For example: + * ``` + * ng-model-options="{ + * updateOn: 'default blur', + * debounce: { 'default': 500, 'blur': 0 } + * }" + * ``` + * - `allowInvalid`: boolean value which indicates that the model can be set with values that did + * not validate correctly instead of the default behavior of setting the model to undefined. + * - `getterSetter`: boolean value which determines whether or not to treat functions bound to + * `ngModel` as getters/setters. + * - `timezone`: Defines the timezone to be used to read/write the `Date` instance in the model for + * ``, ``, ... . It understands UTC/GMT and the + * continental US time zone abbreviations, but for general use, use a time zone offset, for + * example, `'+0430'` (4 hours, 30 minutes east of the Greenwich meridian) + * If not specified, the timezone of the browser will be used. + * + */ + var ngModelOptionsDirective = function() { + NgModelOptionsController.$inject = ['$attrs', '$scope']; + function NgModelOptionsController($attrs, $scope) { + this.$$attrs = $attrs; + this.$$scope = $scope; + } + NgModelOptionsController.prototype = { + $onInit: function() { + var parentOptions = this.parentCtrl ? this.parentCtrl.$options : defaultModelOptions; + var modelOptionsDefinition = this.$$scope.$eval(this.$$attrs.ngModelOptions); + + this.$options = parentOptions.createChild(modelOptionsDefinition); + } + }; + + return { + restrict: 'A', + // ngModelOptions needs to run before ngModel and input directives + priority: 10, + require: {parentCtrl: '?^^ngModelOptions'}, + bindToController: true, + controller: NgModelOptionsController + }; + }; + + +// shallow copy over values from `src` that are not already specified on `dst` + function defaults(dst, src) { + forEach(src, function(value, key) { + if (!isDefined(dst[key])) { + dst[key] = value; + } + }); + } + + /** + * @ngdoc directive + * @name ngNonBindable * @restrict AC * @priority 1000 * @@ -18641,40 +29983,754 @@ * but the one wrapped in `ngNonBindable` is left alone. * * @example - - + +
      Normal: {{1 + 2}}
      Ignored: {{1 + 2}}
      -
      - + + it('should check ng-non-bindable', function() { - expect(using('.doc-example-live').binding('1 + 2')).toBe('3'); - expect(using('.doc-example-live').element('div:last').text()). - toMatch(/1 \+ 2/); + expect(element(by.binding('1 + 2')).getText()).toContain('3'); + expect(element.all(by.css('div')).last().getText()).toMatch(/1 \+ 2/); }); - -
      + + */ var ngNonBindableDirective = ngDirective({ terminal: true, priority: 1000 }); + /* exported ngOptionsDirective */ + + /* global jqLiteRemove */ + + var ngOptionsMinErr = minErr('ngOptions'); + /** * @ngdoc directive - * @name ng.directive:ngPluralize + * @name ngOptions + * @restrict A + * + * @description + * + * The `ngOptions` attribute can be used to dynamically generate a list of `` + * DOM element. + * * `disable`: The result of this expression will be used to disable the rendered `
    2. - */ + this.close = function(dropdownScope, element, appendTo) { + if (openScope === dropdownScope) { + $document.off('click', closeDropdown); + $document.off('keydown', this.keybindFilter); + openScope = null; + } -angular.module('ui.bootstrap.dropdownToggle', []).directive('dropdownToggle', ['$document', '$location', function ($document, $location) { - var openElement = null, - closeMenu = angular.noop; - return { - restrict: 'CA', - link: function(scope, element, attrs) { - scope.$watch('$location.path', function() { closeMenu(); }); - element.parent().bind('click', function() { closeMenu(); }); - element.bind('click', function (event) { + if (!appendTo) { + return; + } - var elementWasOpen = (element === openElement); + var openedDropdowns = openedContainers.get(appendTo); + if (openedDropdowns) { + var dropdownToClose = openedDropdowns.reduce(function(toClose, dropdown) { + if (dropdown.scope === dropdownScope) { + return dropdown; + } - event.preventDefault(); - event.stopPropagation(); - - if (!!openElement) { - closeMenu(); + return toClose; + }, {}); + if (dropdownToClose) { + openedContainers.remove(appendTo, dropdownToClose); } + } + }; - if (!elementWasOpen && !element.hasClass('disabled') && !element.prop('disabled')) { - element.parent().addClass('open'); - openElement = element; - closeMenu = function (event) { - if (event) { - event.preventDefault(); - event.stopPropagation(); - } - $document.unbind('click', closeMenu); - element.parent().removeClass('open'); - closeMenu = angular.noop; - openElement = null; - }; - $document.bind('click', closeMenu); + var closeDropdown = function(evt) { + // This method may still be called during the same mouse event that + // unbound this event handler. So check openScope before proceeding. + if (!openScope || !openScope.isOpen) { return; } + + if (evt && openScope.getAutoClose() === 'disabled') { return; } + + if (evt && evt.which === 3) { return; } + + var toggleElement = openScope.getToggleElement(); + if (evt && toggleElement && toggleElement[0].contains(evt.target)) { + return; + } + + var dropdownElement = openScope.getDropdownElement(); + if (evt && openScope.getAutoClose() === 'outsideClick' && + dropdownElement && dropdownElement[0].contains(evt.target)) { + return; + } + + openScope.focusToggleElement(); + openScope.isOpen = false; + + if (!$rootScope.$$phase) { + openScope.$apply(); + } + }; + + this.keybindFilter = function(evt) { + if (!openScope) { + // see this.close as ESC could have been pressed which kills the scope so we can not proceed + return; + } + + var dropdownElement = openScope.getDropdownElement(); + var toggleElement = openScope.getToggleElement(); + var dropdownElementTargeted = dropdownElement && dropdownElement[0].contains(evt.target); + var toggleElementTargeted = toggleElement && toggleElement[0].contains(evt.target); + if (evt.which === 27) { + evt.stopPropagation(); + openScope.focusToggleElement(); + closeDropdown(); + } else if (openScope.isKeynavEnabled() && [38, 40].indexOf(evt.which) !== -1 && openScope.isOpen && (dropdownElementTargeted || toggleElementTargeted)) { + evt.preventDefault(); + evt.stopPropagation(); + openScope.focusDropdownEntry(evt.which); + } + }; + }]) + + .controller('UibDropdownController', ['$scope', '$element', '$attrs', '$parse', 'uibDropdownConfig', 'uibDropdownService', '$animate', '$uibPosition', '$document', '$compile', '$templateRequest', function($scope, $element, $attrs, $parse, dropdownConfig, uibDropdownService, $animate, $position, $document, $compile, $templateRequest) { + var self = this, + scope = $scope.$new(), // create a child scope so we are not polluting original one + templateScope, + appendToOpenClass = dropdownConfig.appendToOpenClass, + openClass = dropdownConfig.openClass, + getIsOpen, + setIsOpen = angular.noop, + toggleInvoker = $attrs.onToggle ? $parse($attrs.onToggle) : angular.noop, + keynavEnabled = false, + selectedOption = null, + body = $document.find('body'); + + $element.addClass('dropdown'); + + this.init = function() { + if ($attrs.isOpen) { + getIsOpen = $parse($attrs.isOpen); + setIsOpen = getIsOpen.assign; + + $scope.$watch(getIsOpen, function(value) { + scope.isOpen = !!value; + }); + } + + keynavEnabled = angular.isDefined($attrs.keyboardNav); + }; + + this.toggle = function(open) { + scope.isOpen = arguments.length ? !!open : !scope.isOpen; + if (angular.isFunction(setIsOpen)) { + setIsOpen(scope, scope.isOpen); + } + + return scope.isOpen; + }; + + // Allow other directives to watch status + this.isOpen = function() { + return scope.isOpen; + }; + + scope.getToggleElement = function() { + return self.toggleElement; + }; + + scope.getAutoClose = function() { + return $attrs.autoClose || 'always'; //or 'outsideClick' or 'disabled' + }; + + scope.getElement = function() { + return $element; + }; + + scope.isKeynavEnabled = function() { + return keynavEnabled; + }; + + scope.focusDropdownEntry = function(keyCode) { + var elems = self.dropdownMenu ? //If append to body is used. + angular.element(self.dropdownMenu).find('a') : + $element.find('ul').eq(0).find('a'); + + switch (keyCode) { + case 40: { + if (!angular.isNumber(self.selectedOption)) { + self.selectedOption = 0; + } else { + self.selectedOption = self.selectedOption === elems.length - 1 ? + self.selectedOption : + self.selectedOption + 1; + } + break; } - }); + case 38: { + if (!angular.isNumber(self.selectedOption)) { + self.selectedOption = elems.length - 1; + } else { + self.selectedOption = self.selectedOption === 0 ? + 0 : self.selectedOption - 1; + } + break; + } + } + elems[self.selectedOption].focus(); + }; + + scope.getDropdownElement = function() { + return self.dropdownMenu; + }; + + scope.focusToggleElement = function() { + if (self.toggleElement) { + self.toggleElement[0].focus(); + } + }; + + function removeDropdownMenu() { + $element.append(self.dropdownMenu); } - }; -}]); -angular.module('ui.bootstrap.modal', []) + scope.$watch('isOpen', function(isOpen, wasOpen) { + var appendTo = null, + appendToBody = false; + if (angular.isDefined($attrs.dropdownAppendTo)) { + var appendToEl = $parse($attrs.dropdownAppendTo)(scope); + if (appendToEl) { + appendTo = angular.element(appendToEl); + } + } + + if (angular.isDefined($attrs.dropdownAppendToBody)) { + var appendToBodyValue = $parse($attrs.dropdownAppendToBody)(scope); + if (appendToBodyValue !== false) { + appendToBody = true; + } + } + + if (appendToBody && !appendTo) { + appendTo = body; + } + + if (appendTo && self.dropdownMenu) { + if (isOpen) { + appendTo.append(self.dropdownMenu); + $element.on('$destroy', removeDropdownMenu); + } else { + $element.off('$destroy', removeDropdownMenu); + removeDropdownMenu(); + } + } + + if (appendTo && self.dropdownMenu) { + var pos = $position.positionElements($element, self.dropdownMenu, 'bottom-left', true), + css, + rightalign, + scrollbarPadding, + scrollbarWidth = 0; + + css = { + top: pos.top + 'px', + display: isOpen ? 'block' : 'none' + }; + + rightalign = self.dropdownMenu.hasClass('dropdown-menu-right'); + if (!rightalign) { + css.left = pos.left + 'px'; + css.right = 'auto'; + } else { + css.left = 'auto'; + scrollbarPadding = $position.scrollbarPadding(appendTo); + + if (scrollbarPadding.heightOverflow && scrollbarPadding.scrollbarWidth) { + scrollbarWidth = scrollbarPadding.scrollbarWidth; + } + + css.right = window.innerWidth - scrollbarWidth - + (pos.left + $element.prop('offsetWidth')) + 'px'; + } + + // Need to adjust our positioning to be relative to the appendTo container + // if it's not the body element + if (!appendToBody) { + var appendOffset = $position.offset(appendTo); + + css.top = pos.top - appendOffset.top + 'px'; + + if (!rightalign) { + css.left = pos.left - appendOffset.left + 'px'; + } else { + css.right = window.innerWidth - + (pos.left - appendOffset.left + $element.prop('offsetWidth')) + 'px'; + } + } + + self.dropdownMenu.css(css); + } + + var openContainer = appendTo ? appendTo : $element; + var dropdownOpenClass = appendTo ? appendToOpenClass : openClass; + var hasOpenClass = openContainer.hasClass(dropdownOpenClass); + var isOnlyOpen = uibDropdownService.isOnlyOpen($scope, appendTo); + + if (hasOpenClass === !isOpen) { + var toggleClass; + if (appendTo) { + toggleClass = !isOnlyOpen ? 'addClass' : 'removeClass'; + } else { + toggleClass = isOpen ? 'addClass' : 'removeClass'; + } + $animate[toggleClass](openContainer, dropdownOpenClass).then(function() { + if (angular.isDefined(isOpen) && isOpen !== wasOpen) { + toggleInvoker($scope, { open: !!isOpen }); + } + }); + } + + if (isOpen) { + if (self.dropdownMenuTemplateUrl) { + $templateRequest(self.dropdownMenuTemplateUrl).then(function(tplContent) { + templateScope = scope.$new(); + $compile(tplContent.trim())(templateScope, function(dropdownElement) { + var newEl = dropdownElement; + self.dropdownMenu.replaceWith(newEl); + self.dropdownMenu = newEl; + $document.on('keydown', uibDropdownService.keybindFilter); + }); + }); + } else { + $document.on('keydown', uibDropdownService.keybindFilter); + } + + scope.focusToggleElement(); + uibDropdownService.open(scope, $element, appendTo); + } else { + uibDropdownService.close(scope, $element, appendTo); + if (self.dropdownMenuTemplateUrl) { + if (templateScope) { + templateScope.$destroy(); + } + var newEl = angular.element(''); + self.dropdownMenu.replaceWith(newEl); + self.dropdownMenu = newEl; + } + + self.selectedOption = null; + } + + if (angular.isFunction(setIsOpen)) { + setIsOpen($scope, isOpen); + } + }); + }]) + + .directive('uibDropdown', function() { + return { + controller: 'UibDropdownController', + link: function(scope, element, attrs, dropdownCtrl) { + dropdownCtrl.init(); + } + }; + }) + + .directive('uibDropdownMenu', function() { + return { + restrict: 'A', + require: '?^uibDropdown', + link: function(scope, element, attrs, dropdownCtrl) { + if (!dropdownCtrl || angular.isDefined(attrs.dropdownNested)) { + return; + } + + element.addClass('dropdown-menu'); + + var tplUrl = attrs.templateUrl; + if (tplUrl) { + dropdownCtrl.dropdownMenuTemplateUrl = tplUrl; + } + + if (!dropdownCtrl.dropdownMenu) { + dropdownCtrl.dropdownMenu = element; + } + } + }; + }) + + .directive('uibDropdownToggle', function() { + return { + require: '?^uibDropdown', + link: function(scope, element, attrs, dropdownCtrl) { + if (!dropdownCtrl) { + return; + } + + element.addClass('dropdown-toggle'); + + dropdownCtrl.toggleElement = element; + + var toggleDropdown = function(event) { + event.preventDefault(); + + if (!element.hasClass('disabled') && !attrs.disabled) { + scope.$apply(function() { + dropdownCtrl.toggle(); + }); + } + }; + + element.on('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.off('click', toggleDropdown); + }); + } + }; + }); + +angular.module('ui.bootstrap.stackedMap', []) /** * A helper, internal data structure that acts as a map but also allows getting / removing * elements in the LIFO order */ - .factory('$$stackedMap', function () { - return { - createNew: function () { - var stack = []; + .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]; - } + 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; } - }; - } - }; - }) - + }, + 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.pop(); + }, + length: function() { + return stack.length; + } + }; + } + }; + }); +angular.module('ui.bootstrap.modal', ['ui.bootstrap.multiMap', 'ui.bootstrap.stackedMap', 'ui.bootstrap.position']) /** - * A helper directive for the $modal service. It creates a backdrop element. + * Pluggable resolve mechanism for the modal resolve resolution + * Supports UI Router's $resolve service */ - .directive('modalBackdrop', ['$timeout', function ($timeout) { + .provider('$uibResolve', function() { + var resolve = this; + this.resolver = null; + + this.setResolver = function(resolver) { + this.resolver = resolver; + }; + + this.$get = ['$injector', '$q', function($injector, $q) { + var resolver = resolve.resolver ? $injector.get(resolve.resolver) : null; return { - restrict: 'EA', - replace: true, - templateUrl: 'template/modal/backdrop.html', - link: function (scope) { + resolve: function(invocables, locals, parent, self) { + if (resolver) { + return resolver.resolve(invocables, locals, parent, self); + } - scope.animate = false; + var promises = []; - //trigger CSS transitions - $timeout(function () { - scope.animate = true; + angular.forEach(invocables, function(value) { + if (angular.isFunction(value) || angular.isArray(value)) { + promises.push($q.resolve($injector.invoke(value))); + } else if (angular.isString(value)) { + promises.push($q.resolve($injector.get(value))); + } else { + promises.push($q.resolve(value)); + } + }); + + return $q.all(promises).then(function(resolves) { + var resolveObj = {}; + var resolveIter = 0; + angular.forEach(invocables, function(value, key) { + resolveObj[key] = resolves[resolveIter++]; + }); + + return resolveObj; }); } }; + }]; + }) + + /** + * A helper directive for the $modal service. It creates a backdrop element. + */ + .directive('uibModalBackdrop', ['$animate', '$injector', '$uibModalStack', + function($animate, $injector, $modalStack) { + return { + restrict: 'A', + compile: function(tElement, tAttrs) { + tElement.addClass(tAttrs.backdropClass); + return linkFn; + } + }; + + function linkFn(scope, element, attrs) { + if (attrs.modalInClass) { + $animate.addClass(element, attrs.modalInClass); + + scope.$on($modalStack.NOW_CLOSING_EVENT, function(e, setIsAsync) { + var done = setIsAsync(); + if (scope.modalOptions.animation) { + $animate.removeClass(element, attrs.modalInClass).then(done); + } else { + done(); + } + }); + } + } }]) - .directive('modalWindow', ['$modalStack', '$timeout', function ($modalStack, $timeout) { + .directive('uibModalWindow', ['$uibModalStack', '$q', '$animateCss', '$document', + function($modalStack, $q, $animateCss, $document) { return { - restrict: 'EA', scope: { index: '@' }, - replace: true, + restrict: 'A', transclude: true, - templateUrl: 'template/modal/window.html', - link: function (scope, element, attrs) { - scope.windowClass = attrs.windowClass || ''; + templateUrl: function(tElement, tAttrs) { + return tAttrs.templateUrl || 'uib/template/modal/window.html'; + }, + link: function(scope, element, attrs) { + element.addClass(attrs.windowTopClass || ''); + scope.size = attrs.size; - $timeout(function () { - // trigger CSS transitions - scope.animate = true; - // focus a freshly-opened modal - element[0].focus(); - }); - - scope.close = function (evt) { + scope.close = function(evt) { var modal = $modalStack.getTop(); - if (modal && modal.value.backdrop && modal.value.backdrop != 'static' && (evt.target === evt.currentTarget)) { + if (modal && modal.value.backdrop && + modal.value.backdrop !== 'static' && + evt.target === evt.currentTarget) { evt.preventDefault(); evt.stopPropagation(); $modalStack.dismiss(modal.key, 'backdrop click'); } }; + + // moved from template to fix issue #2280 + element.on('click', scope.close); + + // This property is only added to the scope for the purpose of detecting when this directive is rendered. + // We can detect that by using this property in the template associated with this directive and then use + // {@link Attribute#$observe} on it. For more details please see {@link TableColumnResize}. + scope.$isRendered = true; + + // Deferred object that will be resolved when this modal is rendered. + var modalRenderDeferObj = $q.defer(); + // Resolve render promise post-digest + scope.$$postDigest(function() { + modalRenderDeferObj.resolve(); + }); + + modalRenderDeferObj.promise.then(function() { + var animationPromise = null; + + if (attrs.modalInClass) { + animationPromise = $animateCss(element, { + addClass: attrs.modalInClass + }).start(); + + scope.$on($modalStack.NOW_CLOSING_EVENT, function(e, setIsAsync) { + var done = setIsAsync(); + $animateCss(element, { + removeClass: attrs.modalInClass + }).start().then(done); + }); + } + + + $q.when(animationPromise).then(function() { + // Notify {@link $modalStack} that modal is rendered. + var modal = $modalStack.getTop(); + if (modal) { + $modalStack.modalRendered(modal.key); + } + + /** + * If something within the freshly-opened modal already has focus (perhaps via a + * directive that causes focus) then there's no need to try to focus anything. + */ + if (!($document[0].activeElement && element[0].contains($document[0].activeElement))) { + var inputWithAutofocus = element[0].querySelector('[autofocus]'); + /** + * Auto-focusing of a freshly-opened modal element causes any child elements + * with the autofocus attribute to lose focus. This is an issue on touch + * based devices which will show and then hide the onscreen keyboard. + * Attempts to refocus the autofocus element via JavaScript will not reopen + * the onscreen keyboard. Fixed by updated the focusing logic to only autofocus + * the modal element if the modal does not contain an autofocus element. + */ + if (inputWithAutofocus) { + inputWithAutofocus.focus(); + } else { + element[0].focus(); + } + } + }); + }); } }; }]) - .factory('$modalStack', ['$document', '$compile', '$rootScope', '$$stackedMap', - function ($document, $compile, $rootScope, $$stackedMap) { + .directive('uibModalAnimationClass', function() { + return { + compile: function(tElement, tAttrs) { + if (tAttrs.modalAnimation) { + tElement.addClass(tAttrs.uibModalAnimationClass); + } + } + }; + }) - var OPENED_MODAL_CLASS = 'modal-open'; + .directive('uibModalTransclude', ['$animate', function($animate) { + return { + link: function(scope, element, attrs, controller, transclude) { + transclude(scope.$parent, function(clone) { + element.empty(); + $animate.enter(clone, element); + }); + } + }; + }]) - var backdropjqLiteEl, backdropDomEl; - var backdropScope = $rootScope.$new(true); - var openedWindows = $$stackedMap.createNew(); - var $modalStack = {}; + .factory('$uibModalStack', ['$animate', '$animateCss', '$document', + '$compile', '$rootScope', '$q', '$$multiMap', '$$stackedMap', '$uibPosition', + function($animate, $animateCss, $document, $compile, $rootScope, $q, $$multiMap, $$stackedMap, $uibPosition) { + var OPENED_MODAL_CLASS = 'modal-open'; - 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; - } + var backdropDomEl, backdropScope; + var openedWindows = $$stackedMap.createNew(); + var openedClasses = $$multiMap.createNew(); + var $modalStack = { + NOW_CLOSING_EVENT: 'modal.stack.now-closing' + }; + var topModalIndex = 0; + var previousTopOpenedModal = null; + var ARIA_HIDDEN_ATTRIBUTE_NAME = 'data-bootstrap-modal-aria-hidden-count'; + + //Modal focus behavior + var tabbableSelector = 'a[href], area[href], input:not([disabled]):not([tabindex=\'-1\']), ' + + 'button:not([disabled]):not([tabindex=\'-1\']),select:not([disabled]):not([tabindex=\'-1\']), textarea:not([disabled]):not([tabindex=\'-1\']), ' + + 'iframe, object, embed, *[tabindex]:not([tabindex=\'-1\']), *[contenteditable=true]'; + var scrollbarPadding; + var SNAKE_CASE_REGEXP = /[A-Z]/g; + + // TODO: extract into common dependency with tooltip + function snake_case(name) { + var separator = '-'; + return name.replace(SNAKE_CASE_REGEXP, function(letter, pos) { + return (pos ? separator : '') + letter.toLowerCase(); + }); + } + + function isVisible(element) { + return !!(element.offsetWidth || + element.offsetHeight || + element.getClientRects().length); + } + + 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 any backdrop exist, ensure that it's index is always + // right below the top modal + if (topBackdropIndex > -1 && topBackdropIndex < topModalIndex) { + topBackdropIndex = topModalIndex; + } + return topBackdropIndex; + } + + $rootScope.$watch(backdropIndex, function(newBackdropIndex) { + if (backdropScope) { backdropScope.index = newBackdropIndex; - }); + } + }); - function removeModalWindow(modalInstance) { + function removeModalWindow(modalInstance, elementToReceiveFocus) { + var modalWindow = openedWindows.get(modalInstance).value; + var appendToElement = modalWindow.appendTo; - var body = $document.find('body').eq(0); - var modalWindow = openedWindows.get(modalInstance).value; - - //clean up the stack - openedWindows.remove(modalInstance); - - //remove window DOM element - modalWindow.modalDomEl.remove(); - body.toggleClass(OPENED_MODAL_CLASS, openedWindows.length() > 0); - - //remove backdrop if no longer needed - if (backdropDomEl && backdropIndex() == -1) { - backdropDomEl.remove(); - backdropDomEl = undefined; - } - - //destroy scope - modalWindow.modalScope.$destroy(); + //clean up the stack + openedWindows.remove(modalInstance); + previousTopOpenedModal = openedWindows.top(); + if (previousTopOpenedModal) { + topModalIndex = parseInt(previousTopOpenedModal.value.modalDomEl.attr('index'), 10); } - $document.bind('keydown', function (evt) { - var modal; - - if (evt.which === 27) { - modal = openedWindows.top(); - if (modal && modal.value.keyboard) { - $rootScope.$apply(function () { - $modalStack.dismiss(modal.key); - }); + removeAfterAnimate(modalWindow.modalDomEl, modalWindow.modalScope, function() { + var modalBodyClass = modalWindow.openedClass || OPENED_MODAL_CLASS; + openedClasses.remove(modalBodyClass, modalInstance); + var areAnyOpen = openedClasses.hasKey(modalBodyClass); + appendToElement.toggleClass(modalBodyClass, areAnyOpen); + if (!areAnyOpen && scrollbarPadding && scrollbarPadding.heightOverflow && scrollbarPadding.scrollbarWidth) { + if (scrollbarPadding.originalRight) { + appendToElement.css({paddingRight: scrollbarPadding.originalRight + 'px'}); + } else { + appendToElement.css({paddingRight: ''}); } + scrollbarPadding = null; } - }); + toggleTopWindowClass(true); + }, modalWindow.closedDeferred); + checkRemoveBackdrop(); - $modalStack.open = function (modalInstance, modal) { + //move focus to specified element if available, or else to body + if (elementToReceiveFocus && elementToReceiveFocus.focus) { + elementToReceiveFocus.focus(); + } else if (appendToElement.focus) { + appendToElement.focus(); + } + } - openedWindows.add(modalInstance, { - deferred: modal.deferred, - modalScope: modal.scope, - backdrop: modal.backdrop, - keyboard: modal.keyboard + // Add or remove "windowTopClass" from the top window in the stack + function toggleTopWindowClass(toggleSwitch) { + var modalWindow; + + if (openedWindows.length() > 0) { + modalWindow = openedWindows.top().value; + modalWindow.modalDomEl.toggleClass(modalWindow.windowTopClass || '', toggleSwitch); + } + } + + function checkRemoveBackdrop() { + //remove backdrop if no longer needed + if (backdropDomEl && backdropIndex() === -1) { + var backdropScopeRef = backdropScope; + removeAfterAnimate(backdropDomEl, backdropScope, function() { + backdropScopeRef = null; + }); + backdropDomEl = undefined; + backdropScope = undefined; + } + } + + function removeAfterAnimate(domEl, scope, done, closedDeferred) { + var asyncDeferred; + var asyncPromise = null; + var setIsAsync = function() { + if (!asyncDeferred) { + asyncDeferred = $q.defer(); + asyncPromise = asyncDeferred.promise; + } + + return function asyncDone() { + asyncDeferred.resolve(); + }; + }; + scope.$broadcast($modalStack.NOW_CLOSING_EVENT, setIsAsync); + + // Note that it's intentional that asyncPromise might be null. + // That's when setIsAsync has not been called during the + // NOW_CLOSING_EVENT broadcast. + return $q.when(asyncPromise).then(afterAnimating); + + function afterAnimating() { + if (afterAnimating.done) { + return; + } + afterAnimating.done = true; + + $animate.leave(domEl).then(function() { + if (done) { + done(); + } + + domEl.remove(); + if (closedDeferred) { + closedDeferred.resolve(); + } }); - var body = $document.find('body').eq(0); + scope.$destroy(); + } + } - if (backdropIndex() >= 0 && !backdropDomEl) { - backdropjqLiteEl = angular.element('
      '); - backdropDomEl = $compile(backdropjqLiteEl)(backdropScope); - body.append(backdropDomEl); - } + $document.on('keydown', keydownListener); - var angularDomEl = angular.element('
      '); - angularDomEl.attr('window-class', modal.windowClass); - angularDomEl.attr('index', openedWindows.length() - 1); - angularDomEl.html(modal.content); + $rootScope.$on('$destroy', function() { + $document.off('keydown', keydownListener); + }); - var modalDomEl = $compile(angularDomEl)(modal.scope); - openedWindows.top().value.modalDomEl = modalDomEl; - body.append(modalDomEl); - body.addClass(OPENED_MODAL_CLASS); - }; + function keydownListener(evt) { + if (evt.isDefaultPrevented()) { + return evt; + } - $modalStack.close = function (modalInstance, result) { - var modalWindow = openedWindows.get(modalInstance).value; - if (modalWindow) { - modalWindow.deferred.resolve(result); - removeModalWindow(modalInstance); - } - }; - - $modalStack.dismiss = function (modalInstance, reason) { - var modalWindow = openedWindows.get(modalInstance).value; - if (modalWindow) { - modalWindow.deferred.reject(reason); - removeModalWindow(modalInstance); - } - }; - - $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(options.templateUrl, {cache: $templateCache}).then(function (result) { - return result.data; - }); + var modal = openedWindows.top(); + if (modal) { + switch (evt.which) { + case 27: { + if (modal.value.keyboard) { + evt.preventDefault(); + $rootScope.$apply(function() { + $modalStack.dismiss(modal.key, 'escape key press'); + }); + } + break; } - - function getResolvePromises(resolves) { - var promisesArr = []; - angular.forEach(resolves, function (value, key) { - if (angular.isFunction(value) || angular.isArray(value)) { - promisesArr.push($q.when($injector.invoke(value))); + case 9: { + var list = $modalStack.loadFocusElementList(modal); + var focusChanged = false; + if (evt.shiftKey) { + if ($modalStack.isFocusInFirstItem(evt, list) || $modalStack.isModalFocused(evt, modal)) { + focusChanged = $modalStack.focusLastFocusableElement(list); } - }); - 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); + } else { + if ($modalStack.isFocusInLastItem(evt, list)) { + focusChanged = $modalStack.focusFirstFocusableElement(list); } - }; - - //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))); + if (focusChanged) { + evt.preventDefault(); + evt.stopPropagation(); + } + break; + } + } + } + } - templateAndResolvePromise.then(function resolveSuccess(tplAndVars) { + $modalStack.open = function(modalInstance, modal) { + var modalOpener = $document[0].activeElement, + modalBodyClass = modal.openedClass || OPENED_MODAL_CLASS; - var modalScope = (modalOptions.scope || $rootScope).$new(); + toggleTopWindowClass(false); + + // Store the current top first, to determine what index we ought to use + // for the current top modal + previousTopOpenedModal = openedWindows.top(); + + openedWindows.add(modalInstance, { + deferred: modal.deferred, + renderDeferred: modal.renderDeferred, + closedDeferred: modal.closedDeferred, + modalScope: modal.scope, + backdrop: modal.backdrop, + keyboard: modal.keyboard, + openedClass: modal.openedClass, + windowTopClass: modal.windowTopClass, + animation: modal.animation, + appendTo: modal.appendTo + }); + + openedClasses.put(modalBodyClass, modalInstance); + + var appendToElement = modal.appendTo, + currBackdropIndex = backdropIndex(); + + if (currBackdropIndex >= 0 && !backdropDomEl) { + backdropScope = $rootScope.$new(true); + backdropScope.modalOptions = modal; + backdropScope.index = currBackdropIndex; + backdropDomEl = angular.element('
      '); + backdropDomEl.attr({ + 'class': 'modal-backdrop', + 'ng-style': '{\'z-index\': 1040 + (index && 1 || 0) + index*10}', + 'uib-modal-animation-class': 'fade', + 'modal-in-class': 'in' + }); + if (modal.backdropClass) { + backdropDomEl.addClass(modal.backdropClass); + } + + if (modal.animation) { + backdropDomEl.attr('modal-animation', 'true'); + } + $compile(backdropDomEl)(backdropScope); + $animate.enter(backdropDomEl, appendToElement); + if ($uibPosition.isScrollable(appendToElement)) { + scrollbarPadding = $uibPosition.scrollbarPadding(appendToElement); + if (scrollbarPadding.heightOverflow && scrollbarPadding.scrollbarWidth) { + appendToElement.css({paddingRight: scrollbarPadding.right + 'px'}); + } + } + } + + var content; + if (modal.component) { + content = document.createElement(snake_case(modal.component.name)); + content = angular.element(content); + content.attr({ + resolve: '$resolve', + 'modal-instance': '$uibModalInstance', + close: '$close($value)', + dismiss: '$dismiss($value)' + }); + } else { + content = modal.content; + } + + // Set the top modal index based on the index of the previous top modal + topModalIndex = previousTopOpenedModal ? parseInt(previousTopOpenedModal.value.modalDomEl.attr('index'), 10) + 1 : 0; + var angularDomEl = angular.element('
      '); + angularDomEl.attr({ + 'class': 'modal', + 'template-url': modal.windowTemplateUrl, + 'window-top-class': modal.windowTopClass, + 'role': 'dialog', + 'aria-labelledby': modal.ariaLabelledBy, + 'aria-describedby': modal.ariaDescribedBy, + 'size': modal.size, + 'index': topModalIndex, + 'animate': 'animate', + 'ng-style': '{\'z-index\': 1050 + $$topModalIndex*10, display: \'block\'}', + 'tabindex': -1, + 'uib-modal-animation-class': 'fade', + 'modal-in-class': 'in' + }).append(content); + if (modal.windowClass) { + angularDomEl.addClass(modal.windowClass); + } + + if (modal.animation) { + angularDomEl.attr('modal-animation', 'true'); + } + + appendToElement.addClass(modalBodyClass); + if (modal.scope) { + // we need to explicitly add the modal index to the modal scope + // because it is needed by ngStyle to compute the zIndex property. + modal.scope.$$topModalIndex = topModalIndex; + } + $animate.enter($compile(angularDomEl)(modal.scope), appendToElement); + + openedWindows.top().value.modalDomEl = angularDomEl; + openedWindows.top().value.modalOpener = modalOpener; + + applyAriaHidden(angularDomEl); + + function applyAriaHidden(el) { + if (!el || el[0].tagName === 'BODY') { + return; + } + + getSiblings(el).forEach(function(sibling) { + var elemIsAlreadyHidden = sibling.getAttribute('aria-hidden') === 'true', + ariaHiddenCount = parseInt(sibling.getAttribute(ARIA_HIDDEN_ATTRIBUTE_NAME), 10); + + if (!ariaHiddenCount) { + ariaHiddenCount = elemIsAlreadyHidden ? 1 : 0; + } + + sibling.setAttribute(ARIA_HIDDEN_ATTRIBUTE_NAME, ariaHiddenCount + 1); + sibling.setAttribute('aria-hidden', 'true'); + }); + + return applyAriaHidden(el.parent()); + + function getSiblings(el) { + var children = el.parent() ? el.parent().children() : []; + + return Array.prototype.filter.call(children, function(child) { + return child !== el[0]; + }); + } + } + }; + + function broadcastClosing(modalWindow, resultOrReason, closing) { + return !modalWindow.value.modalScope.$broadcast('modal.closing', resultOrReason, closing).defaultPrevented; + } + + function unhideBackgroundElements() { + Array.prototype.forEach.call( + document.querySelectorAll('[' + ARIA_HIDDEN_ATTRIBUTE_NAME + ']'), + function(hiddenEl) { + var ariaHiddenCount = parseInt(hiddenEl.getAttribute(ARIA_HIDDEN_ATTRIBUTE_NAME), 10), + newHiddenCount = ariaHiddenCount - 1; + hiddenEl.setAttribute(ARIA_HIDDEN_ATTRIBUTE_NAME, newHiddenCount); + + if (!newHiddenCount) { + hiddenEl.removeAttribute(ARIA_HIDDEN_ATTRIBUTE_NAME); + hiddenEl.removeAttribute('aria-hidden'); + } + } + ); + } + + $modalStack.close = function(modalInstance, result) { + var modalWindow = openedWindows.get(modalInstance); + unhideBackgroundElements(); + if (modalWindow && broadcastClosing(modalWindow, result, true)) { + modalWindow.value.modalScope.$$uibDestructionScheduled = true; + modalWindow.value.deferred.resolve(result); + removeModalWindow(modalInstance, modalWindow.value.modalOpener); + return true; + } + + return !modalWindow; + }; + + $modalStack.dismiss = function(modalInstance, reason) { + var modalWindow = openedWindows.get(modalInstance); + unhideBackgroundElements(); + if (modalWindow && broadcastClosing(modalWindow, reason, false)) { + modalWindow.value.modalScope.$$uibDestructionScheduled = true; + modalWindow.value.deferred.reject(reason); + removeModalWindow(modalInstance, modalWindow.value.modalOpener); + return true; + } + return !modalWindow; + }; + + $modalStack.dismissAll = function(reason) { + var topModal = this.getTop(); + while (topModal && this.dismiss(topModal.key, reason)) { + topModal = this.getTop(); + } + }; + + $modalStack.getTop = function() { + return openedWindows.top(); + }; + + $modalStack.modalRendered = function(modalInstance) { + var modalWindow = openedWindows.get(modalInstance); + if (modalWindow) { + modalWindow.value.renderDeferred.resolve(); + } + }; + + $modalStack.focusFirstFocusableElement = function(list) { + if (list.length > 0) { + list[0].focus(); + return true; + } + return false; + }; + + $modalStack.focusLastFocusableElement = function(list) { + if (list.length > 0) { + list[list.length - 1].focus(); + return true; + } + return false; + }; + + $modalStack.isModalFocused = function(evt, modalWindow) { + if (evt && modalWindow) { + var modalDomEl = modalWindow.value.modalDomEl; + if (modalDomEl && modalDomEl.length) { + return (evt.target || evt.srcElement) === modalDomEl[0]; + } + } + return false; + }; + + $modalStack.isFocusInFirstItem = function(evt, list) { + if (list.length > 0) { + return (evt.target || evt.srcElement) === list[0]; + } + return false; + }; + + $modalStack.isFocusInLastItem = function(evt, list) { + if (list.length > 0) { + return (evt.target || evt.srcElement) === list[list.length - 1]; + } + return false; + }; + + $modalStack.loadFocusElementList = function(modalWindow) { + if (modalWindow) { + var modalDomE1 = modalWindow.value.modalDomEl; + if (modalDomE1 && modalDomE1.length) { + var elements = modalDomE1[0].querySelectorAll(tabbableSelector); + return elements ? + Array.prototype.filter.call(elements, function(element) { + return isVisible(element); + }) : elements; + } + } + }; + + return $modalStack; + }]) + + .provider('$uibModal', function() { + var $modalProvider = { + options: { + animation: true, + backdrop: true, //can also be false or 'static' + keyboard: true + }, + $get: ['$rootScope', '$q', '$document', '$templateRequest', '$controller', '$uibResolve', '$uibModalStack', + function ($rootScope, $q, $document, $templateRequest, $controller, $uibResolve, $modalStack) { + var $modal = {}; + + function getTemplatePromise(options) { + return options.template ? $q.when(options.template) : + $templateRequest(angular.isFunction(options.templateUrl) ? + options.templateUrl() : options.templateUrl); + } + + var promiseChain = null; + $modal.getPromiseChain = function() { + return promiseChain; + }; + + $modal.open = function(modalOptions) { + var modalResultDeferred = $q.defer(); + var modalOpenedDeferred = $q.defer(); + var modalClosedDeferred = $q.defer(); + var modalRenderDeferred = $q.defer(); + + //prepare an instance of a modal to be injected into controllers and returned to a caller + var modalInstance = { + result: modalResultDeferred.promise, + opened: modalOpenedDeferred.promise, + closed: modalClosedDeferred.promise, + rendered: modalRenderDeferred.promise, + close: function (result) { + return $modalStack.close(modalInstance, result); + }, + dismiss: function (reason) { + return $modalStack.dismiss(modalInstance, reason); + } + }; + + //merge and clean up options + modalOptions = angular.extend({}, $modalProvider.options, modalOptions); + modalOptions.resolve = modalOptions.resolve || {}; + modalOptions.appendTo = modalOptions.appendTo || $document.find('body').eq(0); + + if (!modalOptions.appendTo.length) { + throw new Error('appendTo element not found. Make sure that the element passed is in DOM.'); + } + + //verify options + if (!modalOptions.component && !modalOptions.template && !modalOptions.templateUrl) { + throw new Error('One of component or template or templateUrl options is required.'); + } + + var templateAndResolvePromise; + if (modalOptions.component) { + templateAndResolvePromise = $q.when($uibResolve.resolve(modalOptions.resolve, {}, null, null)); + } else { + templateAndResolvePromise = + $q.all([getTemplatePromise(modalOptions), $uibResolve.resolve(modalOptions.resolve, {}, null, null)]); + } + + function resolveWithTemplate() { + return templateAndResolvePromise; + } + + // Wait for the resolution of the existing promise chain. + // Then switch to our own combined promise dependency (regardless of how the previous modal fared). + // Then add to $modalStack and resolve opened. + // Finally clean up the chain variable if no subsequent modal has overwritten it. + var samePromise; + samePromise = promiseChain = $q.all([promiseChain]) + .then(resolveWithTemplate, resolveWithTemplate) + .then(function resolveSuccess(tplAndVars) { + var providedScope = modalOptions.scope || $rootScope; + + var modalScope = providedScope.$new(); modalScope.$close = modalInstance.close; modalScope.$dismiss = modalInstance.dismiss; - var ctrlInstance, ctrlLocals = {}; - var resolveIter = 1; - - //controllers - if (modalOptions.controller) { - ctrlLocals.$scope = modalScope; - ctrlLocals.$modalInstance = modalInstance; - angular.forEach(modalOptions.resolve, function (value, key) { - ctrlLocals[key] = tplAndVars[resolveIter++]; - }); - - ctrlInstance = $controller(modalOptions.controller, ctrlLocals); - } - - $modalStack.open(modalInstance, { - scope: modalScope, - deferred: modalResultDeferred, - content: tplAndVars[0], - backdrop: modalOptions.backdrop, - keyboard: modalOptions.keyboard, - windowClass: modalOptions.windowClass + modalScope.$on('$destroy', function() { + if (!modalScope.$$uibDestructionScheduled) { + modalScope.$dismiss('$uibUnscheduledDestruction'); + } }); - }, function resolveError(reason) { - modalResultDeferred.reject(reason); - }); + var modal = { + scope: modalScope, + deferred: modalResultDeferred, + renderDeferred: modalRenderDeferred, + closedDeferred: modalClosedDeferred, + animation: modalOptions.animation, + backdrop: modalOptions.backdrop, + keyboard: modalOptions.keyboard, + backdropClass: modalOptions.backdropClass, + windowTopClass: modalOptions.windowTopClass, + windowClass: modalOptions.windowClass, + windowTemplateUrl: modalOptions.windowTemplateUrl, + ariaLabelledBy: modalOptions.ariaLabelledBy, + ariaDescribedBy: modalOptions.ariaDescribedBy, + size: modalOptions.size, + openedClass: modalOptions.openedClass, + appendTo: modalOptions.appendTo + }; - templateAndResolvePromise.then(function () { - modalOpenedDeferred.resolve(true); - }, function () { - modalOpenedDeferred.reject(false); - }); + var component = {}; + var ctrlInstance, ctrlInstantiate, ctrlLocals = {}; - return modalInstance; - }; + if (modalOptions.component) { + constructLocals(component, false, true, false); + component.name = modalOptions.component; + modal.component = component; + } else if (modalOptions.controller) { + constructLocals(ctrlLocals, true, false, true); - return $modal; - }] - }; + // the third param will make the controller instantiate later,private api + // @see https://github.com/angular/angular.js/blob/master/src/ng/controller.js#L126 + ctrlInstantiate = $controller(modalOptions.controller, ctrlLocals, true, modalOptions.controllerAs); + if (modalOptions.controllerAs && modalOptions.bindToController) { + ctrlInstance = ctrlInstantiate.instance; + ctrlInstance.$close = modalScope.$close; + ctrlInstance.$dismiss = modalScope.$dismiss; + angular.extend(ctrlInstance, { + $resolve: ctrlLocals.$scope.$resolve + }, providedScope); + } - return $modalProvider; - }); + ctrlInstance = ctrlInstantiate(); -angular.module('ui.bootstrap.pagination', []) - - .controller('PaginationController', ['$scope', '$attrs', '$parse', '$interpolate', function ($scope, $attrs, $parse, $interpolate) { - var self = this, - setNumPages = $attrs.numPages ? $parse($attrs.numPages).assign : angular.noop; - - this.init = function(defaultItemsPerPage) { - if ($attrs.itemsPerPage) { - $scope.$parent.$watch($parse($attrs.itemsPerPage), function(value) { - self.itemsPerPage = parseInt(value, 10); - $scope.totalPages = self.calculateTotalPages(); - }); - } else { - this.itemsPerPage = defaultItemsPerPage; - } - }; - - this.noPrevious = function() { - return this.page === 1; - }; - this.noNext = function() { - return this.page === $scope.totalPages; - }; - - this.isActive = function(page) { - return this.page === page; - }; - - this.calculateTotalPages = function() { - var totalPages = this.itemsPerPage < 1 ? 1 : Math.ceil($scope.totalItems / this.itemsPerPage); - return Math.max(totalPages || 0, 1); - }; - - this.getAttributeValue = function(attribute, defaultValue, interpolate) { - return angular.isDefined(attribute) ? (interpolate ? $interpolate(attribute)($scope.$parent) : $scope.$parent.$eval(attribute)) : defaultValue; - }; - - this.render = function() { - this.page = parseInt($scope.page, 10) || 1; - if (this.page > 0 && this.page <= $scope.totalPages) { - $scope.pages = this.getPages(this.page, $scope.totalPages); - } - }; - - $scope.selectPage = function(page) { - if ( ! self.isActive(page) && page > 0 && page <= $scope.totalPages) { - $scope.page = page; - $scope.onSelectPage({ page: page }); - } - }; - - $scope.$watch('page', function() { - self.render(); - }); - - $scope.$watch('totalItems', function() { - $scope.totalPages = self.calculateTotalPages(); - }); - - $scope.$watch('totalPages', function(value) { - setNumPages($scope.$parent, value); // Readonly variable - - if ( self.page > value ) { - $scope.selectPage(value); - } else { - self.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, config) { - return { - restrict: 'EA', - scope: { - page: '=', - totalItems: '=', - onSelectPage:' &' - }, - controller: 'PaginationController', - templateUrl: 'template/pagination/pagination.html', - replace: true, - link: function(scope, element, attrs, paginationCtrl) { - - // Setup configuration parameters - var maxSize, - boundaryLinks = paginationCtrl.getAttributeValue(attrs.boundaryLinks, config.boundaryLinks ), - directionLinks = paginationCtrl.getAttributeValue(attrs.directionLinks, config.directionLinks ), - firstText = paginationCtrl.getAttributeValue(attrs.firstText, config.firstText, true), - previousText = paginationCtrl.getAttributeValue(attrs.previousText, config.previousText, true), - nextText = paginationCtrl.getAttributeValue(attrs.nextText, config.nextText, true), - lastText = paginationCtrl.getAttributeValue(attrs.lastText, config.lastText, true), - rotate = paginationCtrl.getAttributeValue(attrs.rotate, config.rotate); - - paginationCtrl.init(config.itemsPerPage); - - 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, isDisabled) { - return { - number: number, - text: text, - active: isActive, - disabled: isDisabled - }; - } - - paginationCtrl.getPages = function(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; + if (angular.isFunction(ctrlInstance.$onInit)) { + ctrlInstance.$onInit(); + } } - } else { - // Visible pages are paginated with maxSize - startPage = ((Math.ceil(currentPage / maxSize) - 1) * maxSize) + 1; - // Adjust last page if limit is exceeded - endPage = Math.min(startPage + maxSize - 1, totalPages); + if (!modalOptions.component) { + modal.content = tplAndVars[0]; + } + + $modalStack.open(modalInstance, modal); + modalOpenedDeferred.resolve(true); + + function constructLocals(obj, template, instanceOnScope, injectable) { + obj.$scope = modalScope; + obj.$scope.$resolve = {}; + if (instanceOnScope) { + obj.$scope.$uibModalInstance = modalInstance; + } else { + obj.$uibModalInstance = modalInstance; + } + + var resolves = template ? tplAndVars[1] : tplAndVars; + angular.forEach(resolves, function(value, key) { + if (injectable) { + obj[key] = value; + } + + obj.$scope.$resolve[key] = value; + }); + } + }, function resolveError(reason) { + modalOpenedDeferred.reject(reason); + modalResultDeferred.reject(reason); + })['finally'](function() { + if (promiseChain === samePromise) { + promiseChain = null; } - } + }); - // Add page number links - for (var number = startPage; number <= endPage; number++) { - var page = makePage(number, number, paginationCtrl.isActive(number), false); - pages.push(page); - } - - // Add links to move between page sets - if ( isMaxSized && ! rotate ) { - if ( startPage > 1 ) { - var previousPageSet = makePage(startPage - 1, '...', false, false); - pages.unshift(previousPageSet); - } - - if ( endPage < totalPages ) { - var nextPageSet = makePage(endPage + 1, '...', false, false); - pages.push(nextPageSet); - } - } - - // Add previous & next links - if (directionLinks) { - var previousPage = makePage(currentPage - 1, previousText, false, paginationCtrl.noPrevious()); - pages.unshift(previousPage); - - var nextPage = makePage(currentPage + 1, nextText, false, paginationCtrl.noNext()); - pages.push(nextPage); - } - - // Add first & last links - if (boundaryLinks) { - var firstPage = makePage(1, firstText, false, paginationCtrl.noPrevious()); - pages.unshift(firstPage); - - var lastPage = makePage(totalPages, lastText, false, paginationCtrl.noNext()); - pages.push(lastPage); - } - - return pages; + return modalInstance; }; + + return $modal; } - }; - }]) + ] + }; - .constant('pagerConfig', { - itemsPerPage: 10, - previousText: '« Previous', - nextText: 'Next »', - align: true - }) + return $modalProvider; + }); - .directive('pager', ['pagerConfig', function(config) { - return { - restrict: 'EA', - scope: { - page: '=', - totalItems: '=', - onSelectPage:' &' - }, - controller: 'PaginationController', - templateUrl: 'template/pagination/pager.html', - replace: true, - link: function(scope, element, attrs, paginationCtrl) { +angular.module('ui.bootstrap.paging', []) +/** + * Helper internal service for generating common controller code between the + * pager and pagination components + */ + .factory('uibPaging', ['$parse', function($parse) { + return { + create: function(ctrl, $scope, $attrs) { + ctrl.setNumPages = $attrs.numPages ? $parse($attrs.numPages).assign : angular.noop; + ctrl.ngModelCtrl = { $setViewValue: angular.noop }; // nullModelCtrl + ctrl._watchers = []; - // Setup configuration parameters - var previousText = paginationCtrl.getAttributeValue(attrs.previousText, config.previousText, true), - nextText = paginationCtrl.getAttributeValue(attrs.nextText, config.nextText, true), - align = paginationCtrl.getAttributeValue(attrs.align, config.align); + ctrl.init = function(ngModelCtrl, config) { + ctrl.ngModelCtrl = ngModelCtrl; + ctrl.config = config; - paginationCtrl.init(config.itemsPerPage); + ngModelCtrl.$render = function() { + ctrl.render(); + }; - // Create page object used in template - function makePage(number, text, isDisabled, isPrevious, isNext) { - return { - number: number, - text: text, - disabled: isDisabled, - previous: ( align && isPrevious ), - next: ( align && isNext ) - }; + if ($attrs.itemsPerPage) { + ctrl._watchers.push($scope.$parent.$watch($attrs.itemsPerPage, function(value) { + ctrl.itemsPerPage = parseInt(value, 10); + $scope.totalPages = ctrl.calculateTotalPages(); + ctrl.updatePage(); + })); + } else { + ctrl.itemsPerPage = config.itemsPerPage; } - paginationCtrl.getPages = function(currentPage) { - return [ - makePage(currentPage - 1, previousText, paginationCtrl.noPrevious(), true, false), - makePage(currentPage + 1, nextText, paginationCtrl.noNext(), false, true) - ]; - }; + $scope.$watch('totalItems', function(newTotal, oldTotal) { + if (angular.isDefined(newTotal) || newTotal !== oldTotal) { + $scope.totalPages = ctrl.calculateTotalPages(); + ctrl.updatePage(); + } + }); + }; + + ctrl.calculateTotalPages = function() { + var totalPages = ctrl.itemsPerPage < 1 ? 1 : Math.ceil($scope.totalItems / ctrl.itemsPerPage); + return Math.max(totalPages || 0, 1); + }; + + ctrl.render = function() { + $scope.page = parseInt(ctrl.ngModelCtrl.$viewValue, 10) || 1; + }; + + $scope.selectPage = function(page, evt) { + if (evt) { + evt.preventDefault(); + } + + var clickAllowed = !$scope.ngDisabled || !evt; + if (clickAllowed && $scope.page !== page && page > 0 && page <= $scope.totalPages) { + if (evt && evt.target) { + evt.target.blur(); + } + ctrl.ngModelCtrl.$setViewValue(page); + ctrl.ngModelCtrl.$render(); + } + }; + + $scope.getText = function(key) { + return $scope[key + 'Text'] || ctrl.config[key + 'Text']; + }; + + $scope.noPrevious = function() { + return $scope.page === 1; + }; + + $scope.noNext = function() { + return $scope.page === $scope.totalPages; + }; + + ctrl.updatePage = function() { + ctrl.setNumPages($scope.$parent, $scope.totalPages); // Readonly variable + + if ($scope.page > $scope.totalPages) { + $scope.selectPage($scope.totalPages); + } else { + ctrl.ngModelCtrl.$render(); + } + }; + + $scope.$on('$destroy', function() { + while (ctrl._watchers.length) { + ctrl._watchers.shift()(); + } + }); + } + }; + }]); + +angular.module('ui.bootstrap.pager', ['ui.bootstrap.paging', 'ui.bootstrap.tabindex']) + + .controller('UibPagerController', ['$scope', '$attrs', 'uibPaging', 'uibPagerConfig', function($scope, $attrs, uibPaging, uibPagerConfig) { + $scope.align = angular.isDefined($attrs.align) ? $scope.$parent.$eval($attrs.align) : uibPagerConfig.align; + + uibPaging.create(this, $scope, $attrs); + }]) + + .constant('uibPagerConfig', { + itemsPerPage: 10, + previousText: '« Previous', + nextText: 'Next »', + align: true + }) + + .directive('uibPager', ['uibPagerConfig', function(uibPagerConfig) { + return { + scope: { + totalItems: '=', + previousText: '@', + nextText: '@', + ngDisabled: '=' + }, + require: ['uibPager', '?ngModel'], + restrict: 'A', + controller: 'UibPagerController', + controllerAs: 'pager', + templateUrl: function(element, attrs) { + return attrs.templateUrl || 'uib/template/pager/pager.html'; + }, + link: function(scope, element, attrs, ctrls) { + element.addClass('pager'); + var paginationCtrl = ctrls[0], ngModelCtrl = ctrls[1]; + + if (!ngModelCtrl) { + return; // do nothing if no ng-model } + + paginationCtrl.init(ngModelCtrl, uibPagerConfig); + } + }; + }]); + +angular.module('ui.bootstrap.pagination', ['ui.bootstrap.paging', 'ui.bootstrap.tabindex']) + .controller('UibPaginationController', ['$scope', '$attrs', '$parse', 'uibPaging', 'uibPaginationConfig', function($scope, $attrs, $parse, uibPaging, uibPaginationConfig) { + var ctrl = this; + // Setup configuration parameters + var maxSize = angular.isDefined($attrs.maxSize) ? $scope.$parent.$eval($attrs.maxSize) : uibPaginationConfig.maxSize, + rotate = angular.isDefined($attrs.rotate) ? $scope.$parent.$eval($attrs.rotate) : uibPaginationConfig.rotate, + forceEllipses = angular.isDefined($attrs.forceEllipses) ? $scope.$parent.$eval($attrs.forceEllipses) : uibPaginationConfig.forceEllipses, + boundaryLinkNumbers = angular.isDefined($attrs.boundaryLinkNumbers) ? $scope.$parent.$eval($attrs.boundaryLinkNumbers) : uibPaginationConfig.boundaryLinkNumbers, + pageLabel = angular.isDefined($attrs.pageLabel) ? function(idx) { return $scope.$parent.$eval($attrs.pageLabel, {$page: idx}); } : angular.identity; + $scope.boundaryLinks = angular.isDefined($attrs.boundaryLinks) ? $scope.$parent.$eval($attrs.boundaryLinks) : uibPaginationConfig.boundaryLinks; + $scope.directionLinks = angular.isDefined($attrs.directionLinks) ? $scope.$parent.$eval($attrs.directionLinks) : uibPaginationConfig.directionLinks; + $attrs.$set('role', 'menu'); + + uibPaging.create(this, $scope, $attrs); + + if ($attrs.maxSize) { + ctrl._watchers.push($scope.$parent.$watch($parse($attrs.maxSize), function(value) { + maxSize = parseInt(value, 10); + ctrl.render(); + })); + } + + // Create page object used in template + function makePage(number, text, isActive) { + return { + number: number, + text: text, + active: isActive }; - }]); + } + + function getPages(currentPage, totalPages) { + var pages = []; + + // Default page limits + var startPage = 1, endPage = totalPages; + var isMaxSized = angular.isDefined(maxSize) && maxSize < totalPages; + + // recompute if maxSize + if (isMaxSized) { + if (rotate) { + // Current page is displayed in the middle of the visible ones + startPage = Math.max(currentPage - Math.floor(maxSize / 2), 1); + endPage = startPage + maxSize - 1; + + // Adjust if limit is exceeded + if (endPage > totalPages) { + endPage = totalPages; + startPage = endPage - maxSize + 1; + } + } else { + // Visible pages are paginated with maxSize + startPage = (Math.ceil(currentPage / maxSize) - 1) * maxSize + 1; + + // Adjust last page if limit is exceeded + endPage = Math.min(startPage + maxSize - 1, totalPages); + } + } + + // Add page number links + for (var number = startPage; number <= endPage; number++) { + var page = makePage(number, pageLabel(number), number === currentPage); + pages.push(page); + } + + // Add links to move between page sets + if (isMaxSized && maxSize > 0 && (!rotate || forceEllipses || boundaryLinkNumbers)) { + if (startPage > 1) { + if (!boundaryLinkNumbers || startPage > 3) { //need ellipsis for all options unless range is too close to beginning + var previousPageSet = makePage(startPage - 1, '...', false); + pages.unshift(previousPageSet); + } + if (boundaryLinkNumbers) { + if (startPage === 3) { //need to replace ellipsis when the buttons would be sequential + var secondPageLink = makePage(2, '2', false); + pages.unshift(secondPageLink); + } + //add the first page + var firstPageLink = makePage(1, '1', false); + pages.unshift(firstPageLink); + } + } + + if (endPage < totalPages) { + if (!boundaryLinkNumbers || endPage < totalPages - 2) { //need ellipsis for all options unless range is too close to end + var nextPageSet = makePage(endPage + 1, '...', false); + pages.push(nextPageSet); + } + if (boundaryLinkNumbers) { + if (endPage === totalPages - 2) { //need to replace ellipsis when the buttons would be sequential + var secondToLastPageLink = makePage(totalPages - 1, totalPages - 1, false); + pages.push(secondToLastPageLink); + } + //add the last page + var lastPageLink = makePage(totalPages, totalPages, false); + pages.push(lastPageLink); + } + } + } + return pages; + } + + var originalRender = this.render; + this.render = function() { + originalRender(); + if ($scope.page > 0 && $scope.page <= $scope.totalPages) { + $scope.pages = getPages($scope.page, $scope.totalPages); + } + }; + }]) + + .constant('uibPaginationConfig', { + itemsPerPage: 10, + boundaryLinks: false, + boundaryLinkNumbers: false, + directionLinks: true, + firstText: 'First', + previousText: 'Previous', + nextText: 'Next', + lastText: 'Last', + rotate: true, + forceEllipses: false + }) + + .directive('uibPagination', ['$parse', 'uibPaginationConfig', function($parse, uibPaginationConfig) { + return { + scope: { + totalItems: '=', + firstText: '@', + previousText: '@', + nextText: '@', + lastText: '@', + ngDisabled:'=' + }, + require: ['uibPagination', '?ngModel'], + restrict: 'A', + controller: 'UibPaginationController', + controllerAs: 'pagination', + templateUrl: function(element, attrs) { + return attrs.templateUrl || 'uib/template/pagination/pagination.html'; + }, + link: function(scope, element, attrs, ctrls) { + element.addClass('pagination'); + var paginationCtrl = ctrls[0], ngModelCtrl = ctrls[1]; + + if (!ngModelCtrl) { + return; // do nothing if no ng-model + } + + paginationCtrl.init(ngModelCtrl, uibPaginationConfig); + } + }; + }]); /** * 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' ] ) +angular.module('ui.bootstrap.tooltip', ['ui.bootstrap.position', 'ui.bootstrap.stackedMap']) /** * The $tooltip service creates tooltip- and popover-like directives as well as * houses global options for them. */ - .provider( '$tooltip', function () { - // The default options tooltip and popover. - var defaultOptions = { - placement: 'top', - animation: true, - popupDelay: 0 - }; + .provider('$uibTooltip', function() { + // The default options tooltip and popover. + var defaultOptions = { + placement: 'top', + placementClassPrefix: '', + animation: true, + popupDelay: 0, + popupCloseDelay: 0, + useContentExp: false + }; - // Default hide triggers for each show trigger - var triggerMap = { - 'mouseenter': 'mouseleave', - 'click': 'click', - 'focus': 'blur' - }; + // Default hide triggers for each show trigger + var triggerMap = { + 'mouseenter': 'mouseleave', + 'click': 'click', + 'outsideClick': 'outsideClick', + 'focus': 'blur', + 'none': '' + }; - // The options specified to the provider globally. - var globalOptions = {}; + // 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 ) { + /** + * `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.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 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(); - }); + /** + * 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', '$document', '$uibPosition', '$interpolate', '$rootScope', '$parse', '$$stackedMap', function($window, $compile, $timeout, $document, $position, $interpolate, $rootScope, $parse, $$stackedMap) { + var openedTooltips = $$stackedMap.createNew(); + $document.on('keyup', keypressListener); + + $rootScope.$on('$destroy', function() { + $document.off('keyup', keypressListener); + }); + + function keypressListener(e) { + if (e.which === 27) { + var last = openedTooltips.top(); + if (last) { + last.value.close(); + last = null; + } + } } - /** - * 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 function $tooltip(ttType, prefix, defaultTriggerShow, options) { + options = angular.extend({}, defaultOptions, globalOptions, options); + /** + * 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).split(' '); + var hide = show.map(function(trigger) { + return triggerMap[trigger] || trigger; + }); return { - restrict: 'EA', - scope: true, - link: function link ( scope, element, attrs ) { - var tooltip = $compile( template )( scope ); + show: show, + hide: hide + }; + } + + var directiveName = snake_case(ttType); + + var startSym = $interpolate.startSymbol(); + var endSym = $interpolate.endSymbol(); + var template = + '
      ' + + '
      '; + + return { + compile: function(tElem, tAttrs) { + var tooltipLinker = $compile(template); + + return function link(scope, element, attrs, tooltipCtrl) { + var tooltip; + var tooltipLinkedScope; var transitionTimeout; - var popupTimeout; - var appendToBody = angular.isDefined( options.appendToBody ) ? options.appendToBody : false; - var triggers = getTriggers( undefined ); - var hasRegisteredTriggers = false; - var hasEnableExp = angular.isDefined(attrs[prefix+'Enable']); + var showTimeout; + var hideTimeout; + var positionTimeout; + var adjustmentTimeout; + var appendToBody = angular.isDefined(options.appendToBody) ? options.appendToBody : false; + var triggers = getTriggers(undefined); + var hasEnableExp = angular.isDefined(attrs[prefix + 'Enable']); + var ttScope = scope.$new(true); + var repositionScheduled = false; + var isOpenParse = angular.isDefined(attrs[prefix + 'IsOpen']) ? $parse(attrs[prefix + 'IsOpen']) : false; + var contentParse = options.useContentExp ? $parse(attrs[ttType]) : false; + var observers = []; + var lastPlacement; - var positionTooltip = function (){ - var position, - ttWidth, - ttHeight, - ttPosition; - // Get the position of the directive element. - position = appendToBody ? $position.offset( element ) : $position.position( element ); + var positionTooltip = function() { + // check if tooltip exists and is not empty + if (!tooltip || !tooltip.html()) { return; } - // Get the height and width of the tooltip so we can center it. - ttWidth = tooltip.prop( 'offsetWidth' ); - ttHeight = tooltip.prop( 'offsetHeight' ); + if (!positionTimeout) { + positionTimeout = $timeout(function() { + var ttPosition = $position.positionElements(element, tooltip, ttScope.placement, appendToBody); + var initialHeight = angular.isDefined(tooltip.offsetHeight) ? tooltip.offsetHeight : tooltip.prop('offsetHeight'); + var elementPos = appendToBody ? $position.offset(element) : $position.position(element); + tooltip.css({ top: ttPosition.top + 'px', left: ttPosition.left + 'px' }); + var placementClasses = ttPosition.placement.split('-'); - // Calculate the tooltip's top and left coordinates to center it with - // this directive. - switch ( scope.tt_placement ) { - case 'right': - ttPosition = { - top: position.top + position.height / 2 - ttHeight / 2, - left: position.left + position.width - }; - break; - case 'bottom': - ttPosition = { - top: position.top + position.height, - left: position.left + position.width / 2 - ttWidth / 2 - }; - break; - case 'left': - ttPosition = { - top: position.top + position.height / 2 - ttHeight / 2, - left: position.left - ttWidth - }; - break; - default: - ttPosition = { - top: position.top - ttHeight, - left: position.left + position.width / 2 - ttWidth / 2 - }; - break; + if (!tooltip.hasClass(placementClasses[0])) { + tooltip.removeClass(lastPlacement.split('-')[0]); + tooltip.addClass(placementClasses[0]); + } + + if (!tooltip.hasClass(options.placementClassPrefix + ttPosition.placement)) { + tooltip.removeClass(options.placementClassPrefix + lastPlacement); + tooltip.addClass(options.placementClassPrefix + ttPosition.placement); + } + + adjustmentTimeout = $timeout(function() { + var currentHeight = angular.isDefined(tooltip.offsetHeight) ? tooltip.offsetHeight : tooltip.prop('offsetHeight'); + var adjustment = $position.adjustTop(placementClasses, elementPos, initialHeight, currentHeight); + if (adjustment) { + tooltip.css(adjustment); + } + adjustmentTimeout = null; + }, 0, false); + + // first time through tt element will have the + // uib-position-measure class or if the placement + // has changed we need to position the arrow. + if (tooltip.hasClass('uib-position-measure')) { + $position.positionArrow(tooltip, ttPosition.placement); + tooltip.removeClass('uib-position-measure'); + } else if (lastPlacement !== ttPosition.placement) { + $position.positionArrow(tooltip, ttPosition.placement); + } + lastPlacement = ttPosition.placement; + + positionTimeout = null; + }, 0, false); } - - ttPosition.top += 'px'; - ttPosition.left += 'px'; - - // Now set the calculated positioning. - tooltip.css( ttPosition ); - }; + // Set up the correct scope to allow transclusion later + ttScope.origScope = scope; + // By default, the tooltip is not open. // TODO add ability to start tooltip opened - scope.tt_isOpen = false; + ttScope.isOpen = false; - function toggleTooltipBind () { - if ( ! scope.tt_isOpen ) { + function toggleTooltipBind() { + if (!ttScope.isOpen) { showTooltipBind(); } else { hideTooltipBind(); @@ -2105,1506 +5112,2676 @@ angular.module( 'ui.bootstrap.tooltip', [ 'ui.bootstrap.position', 'ui.bootstrap // Show the tooltip with delay if specified, otherwise show it immediately function showTooltipBind() { - if(hasEnableExp && !scope.$eval(attrs[prefix+'Enable'])) { + if (hasEnableExp && !scope.$eval(attrs[prefix + 'Enable'])) { return; } - if ( scope.tt_popupDelay ) { - popupTimeout = $timeout( show, scope.tt_popupDelay ); - popupTimeout.then(function(reposition){reposition();}); + + cancelHide(); + prepareTooltip(); + + if (ttScope.popupDelay) { + // Do nothing if the tooltip was already scheduled to pop-up. + // This happens if show is triggered multiple times before any hide is triggered. + if (!showTimeout) { + showTimeout = $timeout(show, ttScope.popupDelay, false); + } } else { - scope.$apply( show )(); + show(); } } - function hideTooltipBind () { - scope.$apply(function () { + function hideTooltipBind() { + cancelShow(); + + if (ttScope.popupCloseDelay) { + if (!hideTimeout) { + hideTimeout = $timeout(hide, ttScope.popupCloseDelay, false); + } + } else { hide(); - }); + } } // Show the tooltip popup element. function show() { - + cancelShow(); + cancelHide(); // Don't show empty tooltips. - if ( ! scope.tt_content ) { + if (!ttScope.content) { return angular.noop; } - // If there is a pending remove transition, we must cancel it, lest the - // tooltip be mysteriously removed. - if ( transitionTimeout ) { - $timeout.cancel( transitionTimeout ); - } - - // 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(); + createTooltip(); // And show the tooltip. - scope.tt_isOpen = true; + ttScope.$evalAsync(function() { + ttScope.isOpen = true; + assignIsOpen(true); + positionTooltip(); + }); + } - // Return positioning function as promise callback for correct - // positioning after draw. - return positionTooltip; + function cancelShow() { + if (showTimeout) { + $timeout.cancel(showTimeout); + showTimeout = null; + } + + if (positionTimeout) { + $timeout.cancel(positionTimeout); + positionTimeout = null; + } } // Hide the tooltip popup element. function hide() { + if (!ttScope) { + return; + } + // First things first: we don't show it anymore. - scope.tt_isOpen = false; - - //if tooltip is going to be shown after delay, we must cancel this - $timeout.cancel( popupTimeout ); - - // 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 ) { - transitionTimeout = $timeout(function () { - tooltip.remove(); - }, 500); - } else { - tooltip.remove(); - } - } - - /** - * 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() { - if (hasRegisteredTriggers) { - 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 ); - } - - hasRegisteredTriggers = true; - }); - - 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(); + ttScope.$evalAsync(function() { + if (ttScope) { + ttScope.isOpen = false; + assignIsOpen(false); + // And now we remove it from the DOM. However, if we have animation, we + // need to wait for it to expire beforehand. + // FIXME: this is a placeholder for a port of the transitions library. + // The fade transition in TWBS is 150ms. + if (ttScope.animation) { + if (!transitionTimeout) { + transitionTimeout = $timeout(removeTooltip, 150, false); + } + } else { + removeTooltip(); + } } }); } + function cancelHide() { + if (hideTimeout) { + $timeout.cancel(hideTimeout); + hideTimeout = null; + } + + if (transitionTimeout) { + $timeout.cancel(transitionTimeout); + transitionTimeout = null; + } + } + + function createTooltip() { + // There can only be one tooltip element per directive shown at once. + if (tooltip) { + return; + } + + tooltipLinkedScope = ttScope.$new(); + tooltip = tooltipLinker(tooltipLinkedScope, function(tooltip) { + if (appendToBody) { + $document.find('body').append(tooltip); + } else { + element.after(tooltip); + } + }); + + openedTooltips.add(ttScope, { + close: hide + }); + + prepObservers(); + } + + function removeTooltip() { + cancelShow(); + cancelHide(); + unregisterObservers(); + + if (tooltip) { + tooltip.remove(); + + tooltip = null; + if (adjustmentTimeout) { + $timeout.cancel(adjustmentTimeout); + } + } + + openedTooltips.remove(ttScope); + + if (tooltipLinkedScope) { + tooltipLinkedScope.$destroy(); + tooltipLinkedScope = null; + } + } + + /** + * Set the initial scope values. Once + * the tooltip is created, the observers + * will be added to keep things in sync. + */ + function prepareTooltip() { + ttScope.title = attrs[prefix + 'Title']; + if (contentParse) { + ttScope.content = contentParse(scope); + } else { + ttScope.content = attrs[ttType]; + } + + ttScope.popupClass = attrs[prefix + 'Class']; + ttScope.placement = angular.isDefined(attrs[prefix + 'Placement']) ? attrs[prefix + 'Placement'] : options.placement; + var placement = $position.parsePlacement(ttScope.placement); + lastPlacement = placement[1] ? placement[0] + '-' + placement[1] : placement[0]; + + var delay = parseInt(attrs[prefix + 'PopupDelay'], 10); + var closeDelay = parseInt(attrs[prefix + 'PopupCloseDelay'], 10); + ttScope.popupDelay = !isNaN(delay) ? delay : options.popupDelay; + ttScope.popupCloseDelay = !isNaN(closeDelay) ? closeDelay : options.popupCloseDelay; + } + + function assignIsOpen(isOpen) { + if (isOpenParse && angular.isFunction(isOpenParse.assign)) { + isOpenParse.assign(scope, isOpen); + } + } + + ttScope.contentExp = function() { + return ttScope.content; + }; + + /** + * Observe the relevant attributes. + */ + attrs.$observe('disabled', function(val) { + if (val) { + cancelShow(); + } + + if (val && ttScope.isOpen) { + hide(); + } + }); + + if (isOpenParse) { + scope.$watch(isOpenParse, function(val) { + if (ttScope && !val === ttScope.isOpen) { + toggleTooltipBind(); + } + }); + } + + function prepObservers() { + observers.length = 0; + + if (contentParse) { + observers.push( + scope.$watch(contentParse, function(val) { + ttScope.content = val; + if (!val && ttScope.isOpen) { + hide(); + } + }) + ); + + observers.push( + tooltipLinkedScope.$watch(function() { + if (!repositionScheduled) { + repositionScheduled = true; + tooltipLinkedScope.$$postDigest(function() { + repositionScheduled = false; + if (ttScope && ttScope.isOpen) { + positionTooltip(); + } + }); + } + }) + ); + } else { + observers.push( + attrs.$observe(ttType, function(val) { + ttScope.content = val; + if (!val && ttScope.isOpen) { + hide(); + } else { + positionTooltip(); + } + }) + ); + } + + observers.push( + attrs.$observe(prefix + 'Title', function(val) { + ttScope.title = val; + if (ttScope.isOpen) { + positionTooltip(); + } + }) + ); + + observers.push( + attrs.$observe(prefix + 'Placement', function(val) { + ttScope.placement = val ? val : options.placement; + if (ttScope.isOpen) { + positionTooltip(); + } + }) + ); + } + + function unregisterObservers() { + if (observers.length) { + angular.forEach(observers, function(observer) { + observer(); + }); + observers.length = 0; + } + } + + // hide tooltips/popovers for outsideClick trigger + function bodyHideTooltipBind(e) { + if (!ttScope || !ttScope.isOpen || !tooltip) { + return; + } + // make sure the tooltip/popover link or tool tooltip/popover itself were not clicked + if (!element[0].contains(e.target) && !tooltip[0].contains(e.target)) { + hideTooltipBind(); + } + } + + // KeyboardEvent handler to hide the tooltip on Escape key press + function hideOnEscapeKey(e) { + if (e.which === 27) { + hideTooltipBind(); + } + } + + var unregisterTriggers = function() { + triggers.show.forEach(function(trigger) { + if (trigger === 'outsideClick') { + element.off('click', toggleTooltipBind); + } else { + element.off(trigger, showTooltipBind); + element.off(trigger, toggleTooltipBind); + } + element.off('keypress', hideOnEscapeKey); + }); + triggers.hide.forEach(function(trigger) { + if (trigger === 'outsideClick') { + $document.off('click', bodyHideTooltipBind); + } else { + element.off(trigger, hideTooltipBind); + } + }); + }; + + function prepTriggers() { + var showTriggers = [], hideTriggers = []; + var val = scope.$eval(attrs[prefix + 'Trigger']); + unregisterTriggers(); + + if (angular.isObject(val)) { + Object.keys(val).forEach(function(key) { + showTriggers.push(key); + hideTriggers.push(val[key]); + }); + triggers = { + show: showTriggers, + hide: hideTriggers + }; + } else { + triggers = getTriggers(val); + } + + if (triggers.show !== 'none') { + triggers.show.forEach(function(trigger, idx) { + if (trigger === 'outsideClick') { + element.on('click', toggleTooltipBind); + $document.on('click', bodyHideTooltipBind); + } else if (trigger === triggers.hide[idx]) { + element.on(trigger, toggleTooltipBind); + } else if (trigger) { + element.on(trigger, showTooltipBind); + element.on(triggers.hide[idx], hideTooltipBind); + } + element.on('keypress', hideOnEscapeKey); + }); + } + } + + prepTriggers(); + + var animation = scope.$eval(attrs[prefix + 'Animation']); + ttScope.animation = angular.isDefined(animation) ? !!animation : options.animation; + + var appendToBodyVal; + var appendKey = prefix + 'AppendToBody'; + if (appendKey in attrs && attrs[appendKey] === undefined) { + appendToBodyVal = true; + } else { + appendToBodyVal = scope.$eval(attrs[appendKey]); + } + + appendToBody = angular.isDefined(appendToBodyVal) ? appendToBodyVal : appendToBody; + // Make sure tooltip is destroyed and removed. scope.$on('$destroy', function onDestroyTooltip() { - $timeout.cancel( transitionTimeout ); - $timeout.cancel( popupTimeout ); unregisterTriggers(); - tooltip.remove(); - tooltip.unbind(); - tooltip = null; + removeTooltip(); + ttScope = null; }); + }; + } + }; + }; + }]; + }) + + // This is mostly ngInclude code but with a custom scope + .directive('uibTooltipTemplateTransclude', [ + '$animate', '$sce', '$compile', '$templateRequest', + function ($animate, $sce, $compile, $templateRequest) { + return { + link: function(scope, elem, attrs) { + var origScope = scope.$eval(attrs.tooltipTemplateTranscludeScope); + + var changeCounter = 0, + currentScope, + previousElement, + currentElement; + + var cleanupLastIncludeContent = function() { + if (previousElement) { + previousElement.remove(); + previousElement = null; + } + + if (currentScope) { + currentScope.$destroy(); + currentScope = null; + } + + if (currentElement) { + $animate.leave(currentElement).then(function() { + previousElement = null; + }); + previousElement = currentElement; + currentElement = null; } }; - }; - }]; - }) - .directive( 'tooltipPopup', function () { - return { - restrict: 'EA', - replace: true, - scope: { content: '@', placement: '@', animation: '&', isOpen: '&' }, - templateUrl: 'template/tooltip/tooltip-popup.html' + scope.$watch($sce.parseAsResourceUrl(attrs.uibTooltipTemplateTransclude), function(src) { + var thisChangeId = ++changeCounter; + + if (src) { + //set the 2nd param to true to ignore the template request error so that the inner + //contents and scope can be cleaned up. + $templateRequest(src, true).then(function(response) { + if (thisChangeId !== changeCounter) { return; } + var newScope = origScope.$new(); + var template = response; + + var clone = $compile(template)(newScope, function(clone) { + cleanupLastIncludeContent(); + $animate.enter(clone, elem); + }); + + currentScope = newScope; + currentElement = clone; + + currentScope.$emit('$includeContentLoaded', src); + }, function() { + if (thisChangeId === changeCounter) { + cleanupLastIncludeContent(); + scope.$emit('$includeContentError', src); + } + }); + scope.$emit('$includeContentRequested', src); + } else { + cleanupLastIncludeContent(); + } + }); + + scope.$on('$destroy', cleanupLastIncludeContent); + } }; - }) - - .directive( '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' - }; - }) + /** + * Note that it's intentional that these classes are *not* applied through $animate. + * They must not be animated as they're expected to be present on the tooltip on + * initialization. + */ + .directive('uibTooltipClasses', ['$uibPosition', function($uibPosition) { + return { + restrict: 'A', + link: function(scope, element, attrs) { + // need to set the primary position so the + // arrow has space during position measure. + // tooltip.positionTooltip() + if (scope.placement) { + // // There are no top-left etc... classes + // // in TWBS, so we need the primary position. + var position = $uibPosition.parsePlacement(scope.placement); + element.addClass(position[0]); + } - .directive( 'tooltipHtmlUnsafe', [ '$tooltip', function ( $tooltip ) { - return $tooltip( 'tooltipHtmlUnsafe', 'tooltip', 'mouseenter' ); - }]); + if (scope.popupClass) { + element.addClass(scope.popupClass); + } + + if (scope.animation) { + element.addClass(attrs.tooltipAnimationClass); + } + } + }; + }]) + + .directive('uibTooltipPopup', function() { + return { + restrict: 'A', + scope: { content: '@' }, + templateUrl: 'uib/template/tooltip/tooltip-popup.html' + }; + }) + + .directive('uibTooltip', [ '$uibTooltip', function($uibTooltip) { + return $uibTooltip('uibTooltip', 'tooltip', 'mouseenter'); + }]) + + .directive('uibTooltipTemplatePopup', function() { + return { + restrict: 'A', + scope: { contentExp: '&', originScope: '&' }, + templateUrl: 'uib/template/tooltip/tooltip-template-popup.html' + }; + }) + + .directive('uibTooltipTemplate', ['$uibTooltip', function($uibTooltip) { + return $uibTooltip('uibTooltipTemplate', 'tooltip', 'mouseenter', { + useContentExp: true + }); + }]) + + .directive('uibTooltipHtmlPopup', function() { + return { + restrict: 'A', + scope: { contentExp: '&' }, + templateUrl: 'uib/template/tooltip/tooltip-html-popup.html' + }; + }) + + .directive('uibTooltipHtml', ['$uibTooltip', function($uibTooltip) { + return $uibTooltip('uibTooltipHtml', 'tooltip', 'mouseenter', { + useContentExp: true + }); + }]); /** * The following features are still outstanding: popup delay, animation as a * function, placement as a function, inside, support for more triggers than - * just mouse enter/leave, html popovers, and selector delegatation. + * just mouse enter/leave, and selector delegatation. */ -angular.module( 'ui.bootstrap.popover', [ 'ui.bootstrap.tooltip' ] ) - .directive( 'popoverPopup', function () { - return { - restrict: 'EA', - replace: true, - scope: { title: '@', content: '@', placement: '@', animation: '&', isOpen: '&' }, - templateUrl: 'template/popover/popover.html' - }; - }) - .directive( 'popover', [ '$compile', '$timeout', '$parse', '$window', '$tooltip', function ( $compile, $timeout, $parse, $window, $tooltip ) { - return $tooltip( 'popover', 'popover', 'click' ); - }]); +angular.module('ui.bootstrap.popover', ['ui.bootstrap.tooltip']) + .directive('uibPopoverTemplatePopup', function() { + return { + restrict: 'A', + scope: { uibTitle: '@', contentExp: '&', originScope: '&' }, + templateUrl: 'uib/template/popover/popover-template.html' + }; + }) -angular.module('ui.bootstrap.progressbar', ['ui.bootstrap.transition']) - - .constant('progressConfig', { - animate: true, - max: 100 - }) - - .controller('ProgressController', ['$scope', '$attrs', 'progressConfig', '$transition', function($scope, $attrs, progressConfig, $transition) { - var self = this, - bars = [], - max = angular.isDefined($attrs.max) ? $scope.$parent.$eval($attrs.max) : progressConfig.max, - animate = angular.isDefined($attrs.animate) ? $scope.$parent.$eval($attrs.animate) : progressConfig.animate; - - this.addBar = function(bar, element) { - var oldValue = 0, index = bar.$parent.$index; - if ( angular.isDefined(index) && bars[index] ) { - oldValue = bars[index].value; - } - bars.push(bar); - - this.update(element, bar.value, oldValue); - - bar.$watch('value', function(value, oldValue) { - if (value !== oldValue) { - self.update(element, value, oldValue); - } - }); - - bar.$on('$destroy', function() { - self.removeBar(bar); - }); - }; - - // Update bar element width - this.update = function(element, newValue, oldValue) { - var percent = this.getPercentage(newValue); - - if (animate) { - element.css('width', this.getPercentage(oldValue) + '%'); - $transition(element, {width: percent + '%'}); - } else { - element.css({'transition': 'none', 'width': percent + '%'}); - } - }; - - this.removeBar = function(bar) { - bars.splice(bars.indexOf(bar), 1); - }; - - this.getPercentage = function(value) { - return Math.round(100 * value / max); - }; - }]) - - .directive('progress', function() { - return { - restrict: 'EA', - replace: true, - transclude: true, - controller: 'ProgressController', - require: 'progress', - scope: {}, - template: '
      ' - //templateUrl: 'template/progressbar/progress.html' // Works in AngularJS 1.2 - }; - }) - - .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])); - } - }; + .directive('uibPopoverTemplate', ['$uibTooltip', function($uibTooltip) { + return $uibTooltip('uibPopoverTemplate', 'popover', 'click', { + useContentExp: true }); -angular.module('ui.bootstrap.rating', []) + }]) - .constant('ratingConfig', { - max: 5, - stateOn: null, - stateOff: null - }) + .directive('uibPopoverHtmlPopup', function() { + return { + restrict: 'A', + scope: { contentExp: '&', uibTitle: '@' }, + templateUrl: 'uib/template/popover/popover-html.html' + }; + }) - .controller('RatingController', ['$scope', '$attrs', '$parse', 'ratingConfig', function($scope, $attrs, $parse, ratingConfig) { + .directive('uibPopoverHtml', ['$uibTooltip', function($uibTooltip) { + return $uibTooltip('uibPopoverHtml', 'popover', 'click', { + useContentExp: true + }); + }]) - this.maxRange = angular.isDefined($attrs.max) ? $scope.$parent.$eval($attrs.max) : ratingConfig.max; - 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; + .directive('uibPopoverPopup', function() { + return { + restrict: 'A', + scope: { uibTitle: '@', content: '@' }, + templateUrl: 'uib/template/popover/popover.html' + }; + }) - this.createRateObjects = function(states) { - var defaultOptions = { - stateOn: this.stateOn, - stateOff: this.stateOff - }; + .directive('uibPopover', ['$uibTooltip', function($uibTooltip) { + return $uibTooltip('uibPopover', 'popover', 'click'); + }]); - for (var i = 0, n = states.length; i < n; i++) { - states[i] = angular.extend({ index: i }, defaultOptions, states[i]); - } - return states; - }; +angular.module('ui.bootstrap.progressbar', []) - // Get objects used in template - $scope.range = angular.isDefined($attrs.ratingStates) ? this.createRateObjects(angular.copy($scope.$parent.$eval($attrs.ratingStates))): this.createRateObjects(new Array(this.maxRange)); + .constant('uibProgressConfig', { + animate: true, + max: 100 + }) - $scope.rate = function(value) { - if ( $scope.readonly || $scope.value === value) { - return; - } + .controller('UibProgressController', ['$scope', '$attrs', 'uibProgressConfig', function($scope, $attrs, progressConfig) { + var self = this, + animate = angular.isDefined($attrs.animate) ? $scope.$parent.$eval($attrs.animate) : progressConfig.animate; - $scope.value = value; - }; + this.bars = []; + $scope.max = getMaxOrDefault(); - $scope.enter = function(value) { - if ( ! $scope.readonly ) { - $scope.val = value; - } - $scope.onHover({value: value}); - }; + this.addBar = function(bar, element, attrs) { + if (!animate) { + element.css({'transition': 'none'}); + } - $scope.reset = function() { - $scope.val = angular.copy($scope.value); - $scope.onLeave(); - }; + this.bars.push(bar); - $scope.$watch('value', function(value) { - $scope.val = value; + bar.max = getMaxOrDefault(); + bar.title = attrs && angular.isDefined(attrs.title) ? attrs.title : 'progressbar'; + + bar.$watch('value', function(value) { + bar.recalculatePercentage(); }); - $scope.readonly = false; - if ($attrs.readonly) { - $scope.$parent.$watch($parse($attrs.readonly), function(value) { - $scope.readonly = !!value; - }); - } - }]) + bar.recalculatePercentage = function() { + var totalPercentage = self.bars.reduce(function(total, bar) { + bar.percent = +(100 * bar.value / bar.max).toFixed(2); + return total + bar.percent; + }, 0); - .directive('rating', function() { - return { - restrict: 'EA', - scope: { - value: '=', - onHover: '&', - onLeave: '&' - }, - controller: 'RatingController', - templateUrl: 'template/rating/rating.html', - replace: true + if (totalPercentage > 100) { + bar.percent -= totalPercentage - 100; + } }; + + bar.$on('$destroy', function() { + element = null; + self.removeBar(bar); + }); + }; + + this.removeBar = function(bar) { + this.bars.splice(this.bars.indexOf(bar), 1); + this.bars.forEach(function (bar) { + bar.recalculatePercentage(); + }); + }; + + //$attrs.$observe('maxParam', function(maxParam) { + $scope.$watch('maxParam', function(maxParam) { + self.bars.forEach(function(bar) { + bar.max = getMaxOrDefault(); + bar.recalculatePercentage(); + }); }); -/** - * @ngdoc overview - * @name ui.bootstrap.tabs - * - * @description - * AngularJS version of the tabs directive. - */ + function getMaxOrDefault () { + return angular.isDefined($scope.maxParam) ? $scope.maxParam : progressConfig.max; + } + }]) + + .directive('uibProgress', function() { + return { + replace: true, + transclude: true, + controller: 'UibProgressController', + require: 'uibProgress', + scope: { + maxParam: '=?max' + }, + templateUrl: 'uib/template/progressbar/progress.html' + }; + }) + + .directive('uibBar', function() { + return { + replace: true, + transclude: true, + require: '^uibProgress', + scope: { + value: '=', + type: '@' + }, + templateUrl: 'uib/template/progressbar/bar.html', + link: function(scope, element, attrs, progressCtrl) { + progressCtrl.addBar(scope, element, attrs); + } + }; + }) + + .directive('uibProgressbar', function() { + return { + replace: true, + transclude: true, + controller: 'UibProgressController', + scope: { + value: '=', + maxParam: '=?max', + type: '@' + }, + templateUrl: 'uib/template/progressbar/progressbar.html', + link: function(scope, element, attrs, progressCtrl) { + progressCtrl.addBar(scope, angular.element(element.children()[0]), {title: attrs.title}); + } + }; + }); + +angular.module('ui.bootstrap.rating', []) + + .constant('uibRatingConfig', { + max: 5, + stateOn: null, + stateOff: null, + enableReset: true, + titles: ['one', 'two', 'three', 'four', 'five'] + }) + + .controller('UibRatingController', ['$scope', '$attrs', 'uibRatingConfig', function($scope, $attrs, ratingConfig) { + var ngModelCtrl = { $setViewValue: angular.noop }, + self = this; + + this.init = function(ngModelCtrl_) { + ngModelCtrl = ngModelCtrl_; + ngModelCtrl.$render = this.render; + + ngModelCtrl.$formatters.push(function(value) { + if (angular.isNumber(value) && value << 0 !== value) { + value = Math.round(value); + } + + return value; + }); + + this.stateOn = angular.isDefined($attrs.stateOn) ? $scope.$parent.$eval($attrs.stateOn) : ratingConfig.stateOn; + this.stateOff = angular.isDefined($attrs.stateOff) ? $scope.$parent.$eval($attrs.stateOff) : ratingConfig.stateOff; + this.enableReset = angular.isDefined($attrs.enableReset) ? + $scope.$parent.$eval($attrs.enableReset) : ratingConfig.enableReset; + var tmpTitles = angular.isDefined($attrs.titles) ? $scope.$parent.$eval($attrs.titles) : ratingConfig.titles; + this.titles = angular.isArray(tmpTitles) && tmpTitles.length > 0 ? + tmpTitles : ratingConfig.titles; + + var ratingStates = angular.isDefined($attrs.ratingStates) ? + $scope.$parent.$eval($attrs.ratingStates) : + new Array(angular.isDefined($attrs.max) ? $scope.$parent.$eval($attrs.max) : ratingConfig.max); + $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, title: this.getTitle(i) }, states[i]); + } + return states; + }; + + this.getTitle = function(index) { + if (index >= this.titles.length) { + return index + 1; + } + + return this.titles[index]; + }; + + $scope.rate = function(value) { + if (!$scope.readonly && value >= 0 && value <= $scope.range.length) { + var newViewValue = self.enableReset && ngModelCtrl.$viewValue === value ? 0 : value; + ngModelCtrl.$setViewValue(newViewValue); + 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; + $scope.title = self.getTitle($scope.value - 1); + }; + }]) + + .directive('uibRating', function() { + return { + require: ['uibRating', 'ngModel'], + restrict: 'A', + scope: { + readonly: '=?readOnly', + onHover: '&', + onLeave: '&' + }, + controller: 'UibRatingController', + templateUrl: 'uib/template/rating/rating.html', + link: function(scope, element, attrs, ctrls) { + var ratingCtrl = ctrls[0], ngModelCtrl = ctrls[1]; + ratingCtrl.init(ngModelCtrl); + } + }; + }); angular.module('ui.bootstrap.tabs', []) - .controller('TabsetController', ['$scope', function TabsetCtrl($scope) { - var ctrl = this, - tabs = ctrl.tabs = $scope.tabs = []; + .controller('UibTabsetController', ['$scope', function ($scope) { + var ctrl = this, + oldIndex; + ctrl.tabs = []; - ctrl.select = function(tab) { - angular.forEach(tabs, function(tab) { - tab.active = false; - }); - tab.active = true; - }; - - ctrl.addTab = function addTab(tab) { - tabs.push(tab); - if (tabs.length === 1 || 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: {}, - 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; - scope.type = angular.isDefined(attrs.type) ? scope.$parent.$eval(attrs.type) : 'tabs'; - } - }; - }) - -/** - * @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: { - 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) { - var getActive, setActive; - if (attrs.active) { - getActive = $parse(attrs.active); - setActive = getActive.assign; - scope.$parent.$watch(getActive, function updateActive(value, oldVal) { - // Avoid re-initializing scope.active as it is already initialized - // below. (watcher is called async during init with value === - // oldVal) - if (value !== oldVal) { - scope.active = !!value; - } - }); - scope.active = getActive(scope.$parent); - } else { - setActive = getActive = angular.noop; - } - - scope.$watch('active', function(active) { - // Note this watcher also initializes and assigns scope.active to the - // attrs.active expression. - setActive(scope.$parent, active); - if (active) { - tabsetCtrl.select(scope); - scope.onSelect(); - } else { - scope.onDeselect(); - } - }); - - 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); - } + ctrl.select = function(index, evt) { + if (!destroyed) { + var previousIndex = findTabIndex(oldIndex); + var previousSelected = ctrl.tabs[previousIndex]; + if (previousSelected) { + previousSelected.tab.onDeselect({ + $event: evt, + $selectedIndex: index }); + if (evt && evt.isDefaultPrevented()) { + return; + } + previousSelected.tab.active = false; } - }; - }]) - .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); - } - }); + var selected = ctrl.tabs[index]; + if (selected) { + selected.tab.onSelect({ + $event: evt }); + selected.tab.active = true; + ctrl.active = selected.index; + oldIndex = selected.index; + } else if (!selected && angular.isDefined(oldIndex)) { + ctrl.active = null; + oldIndex = null; } - }; - 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' - ); } - }) + }; -; + ctrl.addTab = function addTab(tab) { + ctrl.tabs.push({ + tab: tab, + index: tab.index + }); + ctrl.tabs.sort(function(t1, t2) { + if (t1.index > t2.index) { + return 1; + } + + if (t1.index < t2.index) { + return -1; + } + + return 0; + }); + + if (tab.index === ctrl.active || !angular.isDefined(ctrl.active) && ctrl.tabs.length === 1) { + var newActiveIndex = findTabIndex(tab.index); + ctrl.select(newActiveIndex); + } + }; + + ctrl.removeTab = function removeTab(tab) { + var index; + for (var i = 0; i < ctrl.tabs.length; i++) { + if (ctrl.tabs[i].tab === tab) { + index = i; + break; + } + } + + if (ctrl.tabs[index].index === ctrl.active) { + var newActiveTabIndex = index === ctrl.tabs.length - 1 ? + index - 1 : index + 1 % ctrl.tabs.length; + ctrl.select(newActiveTabIndex); + } + + ctrl.tabs.splice(index, 1); + }; + + $scope.$watch('tabset.active', function(val) { + if (angular.isDefined(val) && val !== oldIndex) { + ctrl.select(findTabIndex(val)); + } + }); + + var destroyed; + $scope.$on('$destroy', function() { + destroyed = true; + }); + + function findTabIndex(index) { + for (var i = 0; i < ctrl.tabs.length; i++) { + if (ctrl.tabs[i].index === index) { + return i; + } + } + } + }]) + + .directive('uibTabset', function() { + return { + transclude: true, + replace: true, + scope: {}, + bindToController: { + active: '=?', + type: '@' + }, + controller: 'UibTabsetController', + controllerAs: 'tabset', + templateUrl: function(element, attrs) { + return attrs.templateUrl || 'uib/template/tabs/tabset.html'; + }, + link: function(scope, element, attrs) { + scope.vertical = angular.isDefined(attrs.vertical) ? + scope.$parent.$eval(attrs.vertical) : false; + scope.justified = angular.isDefined(attrs.justified) ? + scope.$parent.$eval(attrs.justified) : false; + } + }; + }) + + .directive('uibTab', ['$parse', function($parse) { + return { + require: '^uibTabset', + replace: true, + templateUrl: function(element, attrs) { + return attrs.templateUrl || 'uib/template/tabs/tab.html'; + }, + transclude: true, + scope: { + heading: '@', + index: '=?', + classes: '@?', + 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 + }, + controllerAs: 'tab', + link: function(scope, elm, attrs, tabsetCtrl, transclude) { + scope.disabled = false; + if (attrs.disable) { + scope.$parent.$watch($parse(attrs.disable), function(value) { + scope.disabled = !! value; + }); + } + + if (angular.isUndefined(attrs.index)) { + if (tabsetCtrl.tabs && tabsetCtrl.tabs.length) { + scope.index = Math.max.apply(null, tabsetCtrl.tabs.map(function(t) { return t.index; })) + 1; + } else { + scope.index = 0; + } + } + + if (angular.isUndefined(attrs.classes)) { + scope.classes = ''; + } + + scope.select = function(evt) { + if (!scope.disabled) { + var index; + for (var i = 0; i < tabsetCtrl.tabs.length; i++) { + if (tabsetCtrl.tabs[i].tab === scope) { + index = i; + break; + } + } + + tabsetCtrl.select(index, evt); + } + }; + + tabsetCtrl.addTab(scope); + scope.$on('$destroy', function() { + tabsetCtrl.removeTab(scope); + }); + + //We need to transclude later, once the content container is ready. + //when this link happens, we're inside a tab heading. + scope.$transcludeFn = transclude; + } + }; + }]) + + .directive('uibTabHeadingTransclude', function() { + return { + restrict: 'A', + require: '^uibTab', + link: function(scope, elm) { + scope.$watch('headingElement', function updateHeadingElement(heading) { + if (heading) { + elm.html(''); + elm.append(heading); + } + }); + } + }; + }) + + .directive('uibTabContentTransclude', function() { + return { + restrict: 'A', + require: '^uibTabset', + link: function(scope, elm, attrs) { + var tab = scope.$eval(attrs.uibTabContentTransclude).tab; + + //Now our tab is ready to be transcluded: both the tab heading area + //and the tab content area are loaded. Transclude 'em both. + 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('uib-tab-heading') || + node.hasAttribute('data-uib-tab-heading') || + node.hasAttribute('x-uib-tab-heading') || + node.tagName.toLowerCase() === 'uib-tab-heading' || + node.tagName.toLowerCase() === 'data-uib-tab-heading' || + node.tagName.toLowerCase() === 'x-uib-tab-heading' || + node.tagName.toLowerCase() === 'uib:tab-heading' + ); + } + }); angular.module('ui.bootstrap.timepicker', []) - .constant('timepickerConfig', { - hourStep: 1, - minuteStep: 1, - showMeridian: true, - meridians: null, - readonlyInput: false, - mousewheel: true - }) + .constant('uibTimepickerConfig', { + hourStep: 1, + minuteStep: 1, + secondStep: 1, + showMeridian: true, + showSeconds: false, + meridians: null, + readonlyInput: false, + mousewheel: true, + arrowkeys: true, + showSpinners: true, + templateUrl: 'uib/template/timepicker/timepicker.html' + }) - .directive('timepicker', ['$parse', '$log', 'timepickerConfig', '$locale', function ($parse, $log, timepickerConfig, $locale) { - return { - restrict: 'EA', - require:'?^ngModel', - replace: true, - scope: {}, - templateUrl: 'template/timepicker/timepicker.html', - link: function(scope, element, attrs, ngModel) { - if ( !ngModel ) { - return; // do nothing if no ng-model - } + .controller('UibTimepickerController', ['$scope', '$element', '$attrs', '$parse', '$log', '$locale', 'uibTimepickerConfig', function($scope, $element, $attrs, $parse, $log, $locale, timepickerConfig) { + var hoursModelCtrl, minutesModelCtrl, secondsModelCtrl; + var selected = new Date(), + watchers = [], + ngModelCtrl = { $setViewValue: angular.noop }, // nullModelCtrl + meridians = angular.isDefined($attrs.meridians) ? $scope.$parent.$eval($attrs.meridians) : timepickerConfig.meridians || $locale.DATETIME_FORMATS.AMPMS, + padHours = angular.isDefined($attrs.padHours) ? $scope.$parent.$eval($attrs.padHours) : true; - var selected = new Date(), - meridians = angular.isDefined(attrs.meridians) ? scope.$parent.$eval(attrs.meridians) : timepickerConfig.meridians || $locale.DATETIME_FORMATS.AMPMS; + $scope.tabindex = angular.isDefined($attrs.tabindex) ? $attrs.tabindex : 0; + $element.removeAttr('tabindex'); - var hourStep = timepickerConfig.hourStep; - if (attrs.hourStep) { - scope.$parent.$watch($parse(attrs.hourStep), function(value) { - hourStep = parseInt(value, 10); - }); - } + this.init = function(ngModelCtrl_, inputs) { + ngModelCtrl = ngModelCtrl_; + ngModelCtrl.$render = this.render; - var minuteStep = timepickerConfig.minuteStep; - if (attrs.minuteStep) { - scope.$parent.$watch($parse(attrs.minuteStep), function(value) { - minuteStep = parseInt(value, 10); - }); - } + ngModelCtrl.$formatters.unshift(function(modelValue) { + return modelValue ? new Date(modelValue) : null; + }); - // 12H / 24H mode - scope.showMeridian = timepickerConfig.showMeridian; - if (attrs.showMeridian) { - scope.$parent.$watch($parse(attrs.showMeridian), function(value) { - scope.showMeridian = !!value; + var hoursInputEl = inputs.eq(0), + minutesInputEl = inputs.eq(1), + secondsInputEl = inputs.eq(2); - if ( ngModel.$error.time ) { - // Evaluate from template - var hours = getHoursFromTemplate(), minutes = getMinutesFromTemplate(); - if (angular.isDefined( hours ) && angular.isDefined( minutes )) { - selected.setHours( hours ); - refresh(); - } - } else { - updateTemplate(); - } - }); - } + hoursModelCtrl = hoursInputEl.controller('ngModel'); + minutesModelCtrl = minutesInputEl.controller('ngModel'); + secondsModelCtrl = secondsInputEl.controller('ngModel'); - // 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; - } + var mousewheel = angular.isDefined($attrs.mousewheel) ? $scope.$parent.$eval($attrs.mousewheel) : timepickerConfig.mousewheel; - if ( scope.showMeridian ) { - if ( hours === 12 ) { - hours = 0; - } - if ( scope.meridian === meridians[1] ) { - hours = hours + 12; - } - } - return hours; - } + if (mousewheel) { + this.setupMousewheelEvents(hoursInputEl, minutesInputEl, secondsInputEl); + } - function getMinutesFromTemplate() { - var minutes = parseInt(scope.minutes, 10); - return ( minutes >= 0 && minutes < 60 ) ? minutes : undefined; - } + var arrowkeys = angular.isDefined($attrs.arrowkeys) ? $scope.$parent.$eval($attrs.arrowkeys) : timepickerConfig.arrowkeys; + if (arrowkeys) { + this.setupArrowkeyEvents(hoursInputEl, minutesInputEl, secondsInputEl); + } - function pad( value ) { - return ( angular.isDefined(value) && value.toString().length < 2 ) ? '0' + value : value; - } + $scope.readonlyInput = angular.isDefined($attrs.readonlyInput) ? $scope.$parent.$eval($attrs.readonlyInput) : timepickerConfig.readonlyInput; + this.setupInputEvents(hoursInputEl, minutesInputEl, secondsInputEl); + }; - // Input elements - var inputs = element.find('input'), hoursInputEl = inputs.eq(0), minutesInputEl = inputs.eq(1); + var hourStep = timepickerConfig.hourStep; + if ($attrs.hourStep) { + watchers.push($scope.$parent.$watch($parse($attrs.hourStep), function(value) { + hourStep = +value; + })); + } - // Respond on mousewheel spin - var mousewheel = (angular.isDefined(attrs.mousewheel)) ? scope.$eval(attrs.mousewheel) : timepickerConfig.mousewheel; - if ( mousewheel ) { + var minuteStep = timepickerConfig.minuteStep; + if ($attrs.minuteStep) { + watchers.push($scope.$parent.$watch($parse($attrs.minuteStep), function(value) { + minuteStep = +value; + })); + } - var isScrollingUp = function(e) { - if (e.originalEvent) { - e = e.originalEvent; - } - //pick correct delta variable depending on event - var delta = (e.wheelDelta) ? e.wheelDelta : -e.deltaY; - return (e.detail || delta > 0); - }; + var min; + watchers.push($scope.$parent.$watch($parse($attrs.min), function(value) { + var dt = new Date(value); + min = isNaN(dt) ? undefined : dt; + })); - hoursInputEl.bind('mousewheel wheel', function(e) { - scope.$apply( (isScrollingUp(e)) ? scope.incrementHours() : scope.decrementHours() ); - e.preventDefault(); - }); + var max; + watchers.push($scope.$parent.$watch($parse($attrs.max), function(value) { + var dt = new Date(value); + max = isNaN(dt) ? undefined : dt; + })); - minutesInputEl.bind('mousewheel wheel', function(e) { - scope.$apply( (isScrollingUp(e)) ? scope.incrementMinutes() : scope.decrementMinutes() ); - e.preventDefault(); - }); - } + var disabled = false; + if ($attrs.ngDisabled) { + watchers.push($scope.$parent.$watch($parse($attrs.ngDisabled), function(value) { + disabled = value; + })); + } - scope.readonlyInput = (angular.isDefined(attrs.readonlyInput)) ? scope.$eval(attrs.readonlyInput) : timepickerConfig.readonlyInput; - if ( ! scope.readonlyInput ) { + $scope.noIncrementHours = function() { + var incrementedSelected = addMinutes(selected, hourStep * 60); + return disabled || incrementedSelected > max || + incrementedSelected < selected && incrementedSelected < min; + }; - var invalidate = function(invalidHours, invalidMinutes) { - ngModel.$setViewValue( null ); - ngModel.$setValidity('time', false); - if (angular.isDefined(invalidHours)) { - scope.invalidHours = invalidHours; - } - if (angular.isDefined(invalidMinutes)) { - scope.invalidMinutes = invalidMinutes; - } - }; + $scope.noDecrementHours = function() { + var decrementedSelected = addMinutes(selected, -hourStep * 60); + return disabled || decrementedSelected < min || + decrementedSelected > selected && decrementedSelected > max; + }; - scope.updateHours = function() { - var hours = getHoursFromTemplate(); + $scope.noIncrementMinutes = function() { + var incrementedSelected = addMinutes(selected, minuteStep); + return disabled || incrementedSelected > max || + incrementedSelected < selected && incrementedSelected < min; + }; - if ( angular.isDefined(hours) ) { - selected.setHours( hours ); - refresh( 'h' ); - } else { - invalidate(true); - } - }; + $scope.noDecrementMinutes = function() { + var decrementedSelected = addMinutes(selected, -minuteStep); + return disabled || decrementedSelected < min || + decrementedSelected > selected && decrementedSelected > max; + }; - hoursInputEl.bind('blur', function(e) { - if ( !scope.validHours && scope.hours < 10) { - scope.$apply( function() { - scope.hours = pad( scope.hours ); - }); - } - }); + $scope.noIncrementSeconds = function() { + var incrementedSelected = addSeconds(selected, secondStep); + return disabled || incrementedSelected > max || + incrementedSelected < selected && incrementedSelected < min; + }; - scope.updateMinutes = function() { - var minutes = getMinutesFromTemplate(); + $scope.noDecrementSeconds = function() { + var decrementedSelected = addSeconds(selected, -secondStep); + return disabled || decrementedSelected < min || + decrementedSelected > selected && decrementedSelected > max; + }; - if ( angular.isDefined(minutes) ) { - selected.setMinutes( minutes ); - refresh( 'm' ); - } else { - invalidate(undefined, true); - } - }; + $scope.noToggleMeridian = function() { + if (selected.getHours() < 12) { + return disabled || addMinutes(selected, 12 * 60) > max; + } - minutesInputEl.bind('blur', function(e) { - if ( !scope.invalidMinutes && scope.minutes < 10 ) { - scope.$apply( function() { - scope.minutes = pad( scope.minutes ); - }); - } - }); - } else { - scope.updateHours = angular.noop; - scope.updateMinutes = angular.noop; - } + return disabled || addMinutes(selected, -12 * 60) < min; + }; - ngModel.$render = function() { - var date = ngModel.$modelValue ? new Date( ngModel.$modelValue ) : null; + var secondStep = timepickerConfig.secondStep; + if ($attrs.secondStep) { + watchers.push($scope.$parent.$watch($parse($attrs.secondStep), function(value) { + secondStep = +value; + })); + } - if ( isNaN(date) ) { - ngModel.$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(); - } - }; + $scope.showSeconds = timepickerConfig.showSeconds; + if ($attrs.showSeconds) { + watchers.push($scope.$parent.$watch($parse($attrs.showSeconds), function(value) { + $scope.showSeconds = !!value; + })); + } - // Call internally when we know that model is valid. - function refresh( keyboardChange ) { - makeValid(); - ngModel.$setViewValue( new Date(selected) ); - updateTemplate( keyboardChange ); - } + // 12H / 24H mode + $scope.showMeridian = timepickerConfig.showMeridian; + if ($attrs.showMeridian) { + watchers.push($scope.$parent.$watch($parse($attrs.showMeridian), function(value) { + $scope.showMeridian = !!value; - function makeValid() { - ngModel.$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() ); + 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(); + } + })); + } - 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) ); - }; + // Get $scope.hours in 24H mode if valid + function getHoursFromTemplate() { + var hours = +$scope.hours; + var valid = $scope.showMeridian ? hours > 0 && hours < 13 : + hours >= 0 && hours < 24; + if (!valid || $scope.hours === '') { + return undefined; + } + + if ($scope.showMeridian) { + if (hours === 12) { + hours = 0; + } + if ($scope.meridian === meridians[1]) { + hours = hours + 12; + } + } + return hours; + } + + function getMinutesFromTemplate() { + var minutes = +$scope.minutes; + var valid = minutes >= 0 && minutes < 60; + if (!valid || $scope.minutes === '') { + return undefined; + } + return minutes; + } + + function getSecondsFromTemplate() { + var seconds = +$scope.seconds; + return seconds >= 0 && seconds < 60 ? seconds : undefined; + } + + function pad(value, noPad) { + if (value === null) { + return ''; + } + + return angular.isDefined(value) && value.toString().length < 2 && !noPad ? + '0' + value : value.toString(); + } + + // Respond on mousewheel spin + this.setupMousewheelEvents = function(hoursInputEl, minutesInputEl, secondsInputEl) { + var isScrollingUp = function(e) { + if (e.originalEvent) { + e = e.originalEvent; + } + //pick correct delta variable depending on event + var delta = e.wheelDelta ? e.wheelDelta : -e.deltaY; + return e.detail || delta > 0; + }; + + hoursInputEl.on('mousewheel wheel', function(e) { + if (!disabled) { + $scope.$apply(isScrollingUp(e) ? $scope.incrementHours() : $scope.decrementHours()); + } + e.preventDefault(); + }); + + minutesInputEl.on('mousewheel wheel', function(e) { + if (!disabled) { + $scope.$apply(isScrollingUp(e) ? $scope.incrementMinutes() : $scope.decrementMinutes()); + } + e.preventDefault(); + }); + + secondsInputEl.on('mousewheel wheel', function(e) { + if (!disabled) { + $scope.$apply(isScrollingUp(e) ? $scope.incrementSeconds() : $scope.decrementSeconds()); + } + e.preventDefault(); + }); + }; + + // Respond on up/down arrowkeys + this.setupArrowkeyEvents = function(hoursInputEl, minutesInputEl, secondsInputEl) { + hoursInputEl.on('keydown', function(e) { + if (!disabled) { + if (e.which === 38) { // up + e.preventDefault(); + $scope.incrementHours(); + $scope.$apply(); + } else if (e.which === 40) { // down + e.preventDefault(); + $scope.decrementHours(); + $scope.$apply(); + } + } + }); + + minutesInputEl.on('keydown', function(e) { + if (!disabled) { + if (e.which === 38) { // up + e.preventDefault(); + $scope.incrementMinutes(); + $scope.$apply(); + } else if (e.which === 40) { // down + e.preventDefault(); + $scope.decrementMinutes(); + $scope.$apply(); + } + } + }); + + secondsInputEl.on('keydown', function(e) { + if (!disabled) { + if (e.which === 38) { // up + e.preventDefault(); + $scope.incrementSeconds(); + $scope.$apply(); + } else if (e.which === 40) { // down + e.preventDefault(); + $scope.decrementSeconds(); + $scope.$apply(); + } + } + }); + }; + + this.setupInputEvents = function(hoursInputEl, minutesInputEl, secondsInputEl) { + if ($scope.readonlyInput) { + $scope.updateHours = angular.noop; + $scope.updateMinutes = angular.noop; + $scope.updateSeconds = angular.noop; + return; + } + + var invalidate = function(invalidHours, invalidMinutes, invalidSeconds) { + ngModelCtrl.$setViewValue(null); + ngModelCtrl.$setValidity('time', false); + if (angular.isDefined(invalidHours)) { + $scope.invalidHours = invalidHours; + if (hoursModelCtrl) { + hoursModelCtrl.$setValidity('hours', false); + } + } + + if (angular.isDefined(invalidMinutes)) { + $scope.invalidMinutes = invalidMinutes; + if (minutesModelCtrl) { + minutesModelCtrl.$setValidity('minutes', false); + } + } + + if (angular.isDefined(invalidSeconds)) { + $scope.invalidSeconds = invalidSeconds; + if (secondsModelCtrl) { + secondsModelCtrl.$setValidity('seconds', false); + } } }; - }]); -angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap.bindHtml']) + $scope.updateHours = function() { + var hours = getHoursFromTemplate(), + minutes = getMinutesFromTemplate(); + + ngModelCtrl.$setDirty(); + + if (angular.isDefined(hours) && angular.isDefined(minutes)) { + selected.setHours(hours); + selected.setMinutes(minutes); + if (selected < min || selected > max) { + invalidate(true); + } else { + refresh('h'); + } + } else { + invalidate(true); + } + }; + + hoursInputEl.on('blur', function(e) { + ngModelCtrl.$setTouched(); + if (modelIsEmpty()) { + makeValid(); + } else if ($scope.hours === null || $scope.hours === '') { + invalidate(true); + } else if (!$scope.invalidHours && $scope.hours < 10) { + $scope.$apply(function() { + $scope.hours = pad($scope.hours, !padHours); + }); + } + }); + + $scope.updateMinutes = function() { + var minutes = getMinutesFromTemplate(), + hours = getHoursFromTemplate(); + + ngModelCtrl.$setDirty(); + + if (angular.isDefined(minutes) && angular.isDefined(hours)) { + selected.setHours(hours); + selected.setMinutes(minutes); + if (selected < min || selected > max) { + invalidate(undefined, true); + } else { + refresh('m'); + } + } else { + invalidate(undefined, true); + } + }; + + minutesInputEl.on('blur', function(e) { + ngModelCtrl.$setTouched(); + if (modelIsEmpty()) { + makeValid(); + } else if ($scope.minutes === null) { + invalidate(undefined, true); + } else if (!$scope.invalidMinutes && $scope.minutes < 10) { + $scope.$apply(function() { + $scope.minutes = pad($scope.minutes); + }); + } + }); + + $scope.updateSeconds = function() { + var seconds = getSecondsFromTemplate(); + + ngModelCtrl.$setDirty(); + + if (angular.isDefined(seconds)) { + selected.setSeconds(seconds); + refresh('s'); + } else { + invalidate(undefined, undefined, true); + } + }; + + secondsInputEl.on('blur', function(e) { + if (modelIsEmpty()) { + makeValid(); + } else if (!$scope.invalidSeconds && $scope.seconds < 10) { + $scope.$apply( function() { + $scope.seconds = pad($scope.seconds); + }); + } + }); + + }; + + this.render = function() { + var date = ngModelCtrl.$viewValue; + + 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; + } + + if (selected < min || selected > max) { + ngModelCtrl.$setValidity('time', false); + $scope.invalidHours = true; + $scope.invalidMinutes = true; + } else { + makeValid(); + } + updateTemplate(); + } + }; + + // Call internally when we know that model is valid. + function refresh(keyboardChange) { + makeValid(); + ngModelCtrl.$setViewValue(new Date(selected)); + updateTemplate(keyboardChange); + } + + function makeValid() { + if (hoursModelCtrl) { + hoursModelCtrl.$setValidity('hours', true); + } + + if (minutesModelCtrl) { + minutesModelCtrl.$setValidity('minutes', true); + } + + if (secondsModelCtrl) { + secondsModelCtrl.$setValidity('seconds', true); + } + + ngModelCtrl.$setValidity('time', true); + $scope.invalidHours = false; + $scope.invalidMinutes = false; + $scope.invalidSeconds = false; + } + + function updateTemplate(keyboardChange) { + if (!ngModelCtrl.$modelValue) { + $scope.hours = null; + $scope.minutes = null; + $scope.seconds = null; + $scope.meridian = meridians[0]; + } else { + var hours = selected.getHours(), + minutes = selected.getMinutes(), + seconds = selected.getSeconds(); + + if ($scope.showMeridian) { + hours = hours === 0 || hours === 12 ? 12 : hours % 12; // Convert 24 to 12 hour system + } + + $scope.hours = keyboardChange === 'h' ? hours : pad(hours, !padHours); + if (keyboardChange !== 'm') { + $scope.minutes = pad(minutes); + } + $scope.meridian = selected.getHours() < 12 ? meridians[0] : meridians[1]; + + if (keyboardChange !== 's') { + $scope.seconds = pad(seconds); + } + $scope.meridian = selected.getHours() < 12 ? meridians[0] : meridians[1]; + } + } + + function addSecondsToSelected(seconds) { + selected = addSeconds(selected, seconds); + refresh(); + } + + function addMinutes(selected, minutes) { + return addSeconds(selected, minutes*60); + } + + function addSeconds(date, seconds) { + var dt = new Date(date.getTime() + seconds * 1000); + var newDate = new Date(date); + newDate.setHours(dt.getHours(), dt.getMinutes(), dt.getSeconds()); + return newDate; + } + + function modelIsEmpty() { + return ($scope.hours === null || $scope.hours === '') && + ($scope.minutes === null || $scope.minutes === '') && + (!$scope.showSeconds || $scope.showSeconds && ($scope.seconds === null || $scope.seconds === '')); + } + + $scope.showSpinners = angular.isDefined($attrs.showSpinners) ? + $scope.$parent.$eval($attrs.showSpinners) : timepickerConfig.showSpinners; + + $scope.incrementHours = function() { + if (!$scope.noIncrementHours()) { + addSecondsToSelected(hourStep * 60 * 60); + } + }; + + $scope.decrementHours = function() { + if (!$scope.noDecrementHours()) { + addSecondsToSelected(-hourStep * 60 * 60); + } + }; + + $scope.incrementMinutes = function() { + if (!$scope.noIncrementMinutes()) { + addSecondsToSelected(minuteStep * 60); + } + }; + + $scope.decrementMinutes = function() { + if (!$scope.noDecrementMinutes()) { + addSecondsToSelected(-minuteStep * 60); + } + }; + + $scope.incrementSeconds = function() { + if (!$scope.noIncrementSeconds()) { + addSecondsToSelected(secondStep); + } + }; + + $scope.decrementSeconds = function() { + if (!$scope.noDecrementSeconds()) { + addSecondsToSelected(-secondStep); + } + }; + + $scope.toggleMeridian = function() { + var minutes = getMinutesFromTemplate(), + hours = getHoursFromTemplate(); + + if (!$scope.noToggleMeridian()) { + if (angular.isDefined(minutes) && angular.isDefined(hours)) { + addSecondsToSelected(12 * 60 * (selected.getHours() < 12 ? 60 : -60)); + } else { + $scope.meridian = $scope.meridian === meridians[0] ? meridians[1] : meridians[0]; + } + } + }; + + $scope.blur = function() { + ngModelCtrl.$setTouched(); + }; + + $scope.$on('$destroy', function() { + while (watchers.length) { + watchers.shift()(); + } + }); + }]) + + .directive('uibTimepicker', ['uibTimepickerConfig', function(uibTimepickerConfig) { + return { + require: ['uibTimepicker', '?^ngModel'], + restrict: 'A', + controller: 'UibTimepickerController', + controllerAs: 'timepicker', + scope: {}, + templateUrl: function(element, attrs) { + return attrs.templateUrl || uibTimepickerConfig.templateUrl; + }, + link: function(scope, element, attrs, ctrls) { + var timepickerCtrl = ctrls[0], ngModelCtrl = ctrls[1]; + + if (ngModelCtrl) { + timepickerCtrl.init(ngModelCtrl, element.find('input')); + } + } + }; + }]); + +angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.debounce', 'ui.bootstrap.position']) /** * A helper service that can parse typeahead's syntax (string provided by users) * Extracted to a separate service for ease of unit testing */ - .factory('typeaheadParser', ['$parse', function ($parse) { - - // 00000111000000000000022200000000000000003333333333333330000000000044000 - var TYPEAHEAD_REGEXP = /^\s*(.*?)(?:\s+as\s+(.*?))?\s+for\s+(?:([\$\w][\$\w\d]*))\s+in\s+(.*)$/; - - return { - parse:function (input) { - - var match = input.match(TYPEAHEAD_REGEXP), modelMapper, viewMapper, source; - 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]) - }; + .factory('uibTypeaheadParser', ['$parse', function($parse) { + // 000001111111100000000000002222222200000000000000003333333333333330000000000044444444000 + 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 + '".'); } - }; - }]) - - .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 ? $parse(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; - - //pop-up element used to display matches - var popUpEl = angular.element('
      '); - popUpEl.attr({ - 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); - } - - //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(); - }); - - var resetMatches = function() { - scope.matches = []; - scope.activeIdx = -1; - }; - - 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 - if (inputValue === modelCtrl.$viewValue && hasFocus) { - if (matches.length > 0) { - - scope.activeIdx = 0; - scope.matches.length = 0; - - //transform labels - for(var i=0; i= minSearch) { - if (waitTime > 0) { - if (timeoutPromise) { - $timeout.cancel(timeoutPromise);//cancel previous timeout - } - timeoutPromise = $timeout(function () { - getMatchesAsync(inputValue); - }, waitTime); - } else { - getMatchesAsync(inputValue); - } - } else { - isLoadingSetter(originalScope, false); - 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 mach was selected via a mouse click event - element[0].focus(); - }; - - //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); - } - } + itemName: match[3], + source: $parse(match[4]), + viewMapper: $parse(match[2] || match[1]), + modelMapper: $parse(match[1]) }; + } + }; + }]) - }]) + .controller('UibTypeaheadController', ['$scope', '$element', '$attrs', '$compile', '$parse', '$q', '$timeout', '$document', '$window', '$rootScope', '$$debounce', '$uibPosition', 'uibTypeaheadParser', + function(originalScope, element, attrs, $compile, $parse, $q, $timeout, $document, $window, $rootScope, $$debounce, $position, typeaheadParser) { + var HOT_KEYS = [9, 13, 27, 38, 40]; + var eventDebounceTime = 200; + var modelCtrl, ngModelOptions; + //SUPPORTED ATTRIBUTES (OPTIONS) - .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"); + //minimal no of characters that needs to be entered before typeahead kicks-in + var minLength = originalScope.$eval(attrs.typeaheadMinLength); + if (!minLength && minLength !== 0) { + minLength = 1; } - return function(matchItem, query) { - return query ? matchItem.replace(new RegExp(escapeRegexp(query), 'gi'), '$&') : matchItem; + originalScope.$watch(attrs.typeaheadMinLength, function (newVal) { + minLength = !newVal && newVal !== 0 ? 1 : newVal; + }); + + //minimal wait time after last character typed before typeahead kicks-in + var waitTime = originalScope.$eval(attrs.typeaheadWaitMs) || 0; + + //should it restrict model values to the ones selected from the popup only? + var isEditable = originalScope.$eval(attrs.typeaheadEditable) !== false; + originalScope.$watch(attrs.typeaheadEditable, function (newVal) { + isEditable = newVal !== false; + }); + + //binding to a variable that indicates if matches are being retrieved asynchronously + var isLoadingSetter = $parse(attrs.typeaheadLoading).assign || angular.noop; + + //a function to determine if an event should cause selection + var isSelectEvent = attrs.typeaheadShouldSelect ? $parse(attrs.typeaheadShouldSelect) : function(scope, vals) { + var evt = vals.$event; + return evt.which === 13 || evt.which === 9; }; - }); -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" + - "
      "); + + //a callback executed when a match is selected + var onSelectCallback = $parse(attrs.typeaheadOnSelect); + + //should it select highlighted popup value when losing focus? + var isSelectOnBlur = angular.isDefined(attrs.typeaheadSelectOnBlur) ? originalScope.$eval(attrs.typeaheadSelectOnBlur) : false; + + //binding to a variable that indicates if there were no results after the query is completed + var isNoResultsSetter = $parse(attrs.typeaheadNoResults).assign || angular.noop; + + var inputFormatter = attrs.typeaheadInputFormatter ? $parse(attrs.typeaheadInputFormatter) : undefined; + + var appendToBody = attrs.typeaheadAppendToBody ? originalScope.$eval(attrs.typeaheadAppendToBody) : false; + + var appendTo = attrs.typeaheadAppendTo ? + originalScope.$eval(attrs.typeaheadAppendTo) : null; + + var focusFirst = originalScope.$eval(attrs.typeaheadFocusFirst) !== false; + + //If input matches an item of the list exactly, select it automatically + var selectOnExact = attrs.typeaheadSelectOnExact ? originalScope.$eval(attrs.typeaheadSelectOnExact) : false; + + //binding to a variable that indicates if dropdown is open + var isOpenSetter = $parse(attrs.typeaheadIsOpen).assign || angular.noop; + + var showHint = originalScope.$eval(attrs.typeaheadShowHint) || false; + + //INTERNAL VARIABLES + + //model setter executed upon match selection + var parsedModel = $parse(attrs.ngModel); + var invokeModelSetter = $parse(attrs.ngModel + '($$$p)'); + var $setModelValue = function(scope, newValue) { + if (angular.isFunction(parsedModel(originalScope)) && + ngModelOptions.getOption('getterSetter')) { + return invokeModelSetter(scope, {$$$p: newValue}); + } + + return parsedModel.assign(scope, newValue); + }; + + //expressions used by typeahead + var parserResult = typeaheadParser.parse(attrs.uibTypeahead); + + var hasFocus; + + //Used to avoid bug in iOS webview where iOS keyboard does not fire + //mousedown & mouseup events + //Issue #3699 + var selected; + + //create a child scope for the typeahead directive so we are not polluting original scope + //with typeahead-specific data (matches, query etc.) + var scope = originalScope.$new(); + var offDestroy = originalScope.$on('$destroy', function() { + scope.$destroy(); + }); + scope.$on('$destroy', offDestroy); + + // WAI-ARIA + var popupId = 'typeahead-' + scope.$id + '-' + Math.floor(Math.random() * 10000); + element.attr({ + 'aria-autocomplete': 'list', + 'aria-expanded': false, + 'aria-owns': popupId + }); + + var inputsContainer, hintInputElem; + //add read-only input to show hint + if (showHint) { + inputsContainer = angular.element('
      '); + inputsContainer.css('position', 'relative'); + element.after(inputsContainer); + hintInputElem = element.clone(); + hintInputElem.attr('placeholder', ''); + hintInputElem.attr('tabindex', '-1'); + hintInputElem.val(''); + hintInputElem.css({ + 'position': 'absolute', + 'top': '0px', + 'left': '0px', + 'border-color': 'transparent', + 'box-shadow': 'none', + 'opacity': 1, + 'background': 'none 0% 0% / auto repeat scroll padding-box border-box rgb(255, 255, 255)', + 'color': '#999' + }); + element.css({ + 'position': 'relative', + 'vertical-align': 'top', + 'background-color': 'transparent' + }); + + if (hintInputElem.attr('id')) { + hintInputElem.removeAttr('id'); // remove duplicate id if present. + } + inputsContainer.append(hintInputElem); + hintInputElem.after(element); + } + + //pop-up element used to display matches + var popUpEl = angular.element('
      '); + popUpEl.attr({ + id: popupId, + matches: 'matches', + active: 'activeIdx', + select: 'select(activeIdx, evt)', + 'move-in-progress': 'moveInProgress', + query: 'query', + position: 'position', + 'assign-is-open': 'assignIsOpen(isOpen)', + debounce: 'debounceUpdate' + }); + //custom item template + if (angular.isDefined(attrs.typeaheadTemplateUrl)) { + popUpEl.attr('template-url', attrs.typeaheadTemplateUrl); + } + + if (angular.isDefined(attrs.typeaheadPopupTemplateUrl)) { + popUpEl.attr('popup-template-url', attrs.typeaheadPopupTemplateUrl); + } + + var resetHint = function() { + if (showHint) { + hintInputElem.val(''); + } + }; + + var resetMatches = function() { + scope.matches = []; + scope.activeIdx = -1; + element.attr('aria-expanded', false); + resetHint(); + }; + + 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 inputIsExactMatch = function(inputValue, index) { + if (scope.matches.length > index && inputValue) { + return inputValue.toUpperCase() === scope.matches[index].label.toUpperCase(); + } + + return false; + }; + + var getMatchesAsync = function(inputValue, evt) { + var locals = {$viewValue: inputValue}; + isLoadingSetter(originalScope, true); + isNoResultsSetter(originalScope, false); + $q.when(parserResult.source(originalScope, locals)).then(function(matches) { + //it might happen that several async queries were in progress if a user were typing fast + //but we are interested only in responses that correspond to the current view value + var onCurrentRequest = inputValue === modelCtrl.$viewValue; + if (onCurrentRequest && hasFocus) { + if (matches && matches.length > 0) { + scope.activeIdx = focusFirst ? 0 : -1; + isNoResultsSetter(originalScope, false); + scope.matches.length = 0; + + //transform labels + for (var i = 0; i < matches.length; i++) { + locals[parserResult.itemName] = matches[i]; + scope.matches.push({ + id: getMatchId(i), + label: parserResult.viewMapper(scope, locals), + model: matches[i] + }); + } + + scope.query = inputValue; + //position pop-up with matches - we need to re-calculate its position each time we are opening a window + //with matches as a pop-up might be absolute-positioned and position of an input might have changed on a page + //due to other elements being rendered + recalculatePosition(); + + element.attr('aria-expanded', true); + + //Select the single remaining option if user input matches + if (selectOnExact && scope.matches.length === 1 && inputIsExactMatch(inputValue, 0)) { + if (angular.isNumber(scope.debounceUpdate) || angular.isObject(scope.debounceUpdate)) { + $$debounce(function() { + scope.select(0, evt); + }, angular.isNumber(scope.debounceUpdate) ? scope.debounceUpdate : scope.debounceUpdate['default']); + } else { + scope.select(0, evt); + } + } + + if (showHint) { + var firstLabel = scope.matches[0].label; + if (angular.isString(inputValue) && + inputValue.length > 0 && + firstLabel.slice(0, inputValue.length).toUpperCase() === inputValue.toUpperCase()) { + hintInputElem.val(inputValue + firstLabel.slice(inputValue.length)); + } else { + hintInputElem.val(''); + } + } + } else { + resetMatches(); + isNoResultsSetter(originalScope, true); + } + } + if (onCurrentRequest) { + isLoadingSetter(originalScope, false); + } + }, function() { + resetMatches(); + isLoadingSetter(originalScope, false); + isNoResultsSetter(originalScope, true); + }); + }; + + // bind events only if appendToBody params exist - performance feature + if (appendToBody) { + angular.element($window).on('resize', fireRecalculating); + $document.find('body').on('scroll', fireRecalculating); + } + + // Declare the debounced function outside recalculating for + // proper debouncing + var debouncedRecalculate = $$debounce(function() { + // if popup is visible + if (scope.matches.length) { + recalculatePosition(); + } + + scope.moveInProgress = false; + }, eventDebounceTime); + + // Default progress type + scope.moveInProgress = false; + + function fireRecalculating() { + if (!scope.moveInProgress) { + scope.moveInProgress = true; + scope.$digest(); + } + + debouncedRecalculate(); + } + + // recalculate actual position and set new values to scope + // after digest loop is popup in right position + function recalculatePosition() { + scope.position = appendToBody ? $position.offset(element) : $position.position(element); + scope.position.top += element.prop('offsetHeight'); + } + + //we need to propagate user's query so we can higlight matches + scope.query = undefined; + + //Declare the timeout promise var outside the function scope so that stacked calls can be cancelled later + var timeoutPromise; + + var scheduleSearchWithTimeout = function(inputValue) { + timeoutPromise = $timeout(function() { + getMatchesAsync(inputValue); + }, waitTime); + }; + + var cancelPreviousTimeout = function() { + if (timeoutPromise) { + $timeout.cancel(timeoutPromise); + } + }; + + resetMatches(); + + scope.assignIsOpen = function (isOpen) { + isOpenSetter(originalScope, isOpen); + }; + + scope.select = function(activeIdx, evt) { + //called from within the $digest() cycle + var locals = {}; + var model, item; + + selected = true; + locals[parserResult.itemName] = item = scope.matches[activeIdx].model; + model = parserResult.modelMapper(originalScope, locals); + $setModelValue(originalScope, model); + modelCtrl.$setValidity('editable', true); + modelCtrl.$setValidity('parse', true); + + onSelectCallback(originalScope, { + $item: item, + $model: model, + $label: parserResult.viewMapper(originalScope, locals), + $event: evt + }); + + resetMatches(); + + //return focus to the input element if a match was selected via a mouse click event + // use timeout to avoid $rootScope:inprog error + if (scope.$eval(attrs.typeaheadFocusOnSelect) !== false) { + $timeout(function() { element[0].focus(); }, 0, false); + } + }; + + //bind keyboard events: arrows up(38) / down(40), enter(13) and tab(9), esc(27) + element.on('keydown', function(evt) { + //typeahead is open and an "interesting" key was pressed + if (scope.matches.length === 0 || HOT_KEYS.indexOf(evt.which) === -1) { + return; + } + + var shouldSelect = isSelectEvent(originalScope, {$event: evt}); + + /** + * if there's nothing selected (i.e. focusFirst) and enter or tab is hit + * or + * shift + tab is pressed to bring focus to the previous element + * then clear the results + */ + if (scope.activeIdx === -1 && shouldSelect || evt.which === 9 && !!evt.shiftKey) { + resetMatches(); + scope.$digest(); + return; + } + + evt.preventDefault(); + var target; + switch (evt.which) { + case 27: // escape + evt.stopPropagation(); + + resetMatches(); + originalScope.$digest(); + break; + case 38: // up arrow + scope.activeIdx = (scope.activeIdx > 0 ? scope.activeIdx : scope.matches.length) - 1; + scope.$digest(); + target = popUpEl[0].querySelectorAll('.uib-typeahead-match')[scope.activeIdx]; + target.parentNode.scrollTop = target.offsetTop; + break; + case 40: // down arrow + scope.activeIdx = (scope.activeIdx + 1) % scope.matches.length; + scope.$digest(); + target = popUpEl[0].querySelectorAll('.uib-typeahead-match')[scope.activeIdx]; + target.parentNode.scrollTop = target.offsetTop; + break; + default: + if (shouldSelect) { + scope.$apply(function() { + if (angular.isNumber(scope.debounceUpdate) || angular.isObject(scope.debounceUpdate)) { + $$debounce(function() { + scope.select(scope.activeIdx, evt); + }, angular.isNumber(scope.debounceUpdate) ? scope.debounceUpdate : scope.debounceUpdate['default']); + } else { + scope.select(scope.activeIdx, evt); + } + }); + } + } + }); + + element.on('focus', function (evt) { + hasFocus = true; + if (minLength === 0 && !modelCtrl.$viewValue) { + $timeout(function() { + getMatchesAsync(modelCtrl.$viewValue, evt); + }, 0); + } + }); + + element.on('blur', function(evt) { + if (isSelectOnBlur && scope.matches.length && scope.activeIdx !== -1 && !selected) { + selected = true; + scope.$apply(function() { + if (angular.isObject(scope.debounceUpdate) && angular.isNumber(scope.debounceUpdate.blur)) { + $$debounce(function() { + scope.select(scope.activeIdx, evt); + }, scope.debounceUpdate.blur); + } else { + scope.select(scope.activeIdx, evt); + } + }); + } + if (!isEditable && modelCtrl.$error.editable) { + modelCtrl.$setViewValue(); + scope.$apply(function() { + // Reset validity as we are clearing + modelCtrl.$setValidity('editable', true); + modelCtrl.$setValidity('parse', true); + }); + element.val(''); + } + hasFocus = false; + selected = false; + }); + + // Keep reference to click handler to unbind it. + var dismissClickHandler = function(evt) { + // Issue #3973 + // Firefox treats right click as a click on document + if (element[0] !== evt.target && evt.which !== 3 && scope.matches.length !== 0) { + resetMatches(); + if (!$rootScope.$$phase) { + originalScope.$digest(); + } + } + }; + + $document.on('click', dismissClickHandler); + + originalScope.$on('$destroy', function() { + $document.off('click', dismissClickHandler); + if (appendToBody || appendTo) { + $popup.remove(); + } + + if (appendToBody) { + angular.element($window).off('resize', fireRecalculating); + $document.find('body').off('scroll', fireRecalculating); + } + // Prevent jQuery cache memory leak + popUpEl.remove(); + + if (showHint) { + inputsContainer.remove(); + } + }); + + var $popup = $compile(popUpEl)(scope); + + if (appendToBody) { + $document.find('body').append($popup); + } else if (appendTo) { + angular.element(appendTo).eq(0).append($popup); + } else { + element.after($popup); + } + + this.init = function(_modelCtrl) { + modelCtrl = _modelCtrl; + ngModelOptions = extractOptions(modelCtrl); + + scope.debounceUpdate = $parse(ngModelOptions.getOption('debounce'))(originalScope); + + //plug into $parsers pipeline to open a typeahead on view changes initiated from DOM + //$parsers kick-in on all the changes coming from the view as well as manually triggered by $setViewValue + modelCtrl.$parsers.unshift(function(inputValue) { + hasFocus = true; + + if (minLength === 0 || inputValue && inputValue.length >= minLength) { + if (waitTime > 0) { + cancelPreviousTimeout(); + scheduleSearchWithTimeout(inputValue); + } else { + getMatchesAsync(inputValue); + } + } else { + isLoadingSetter(originalScope, false); + cancelPreviousTimeout(); + resetMatches(); + } + + if (isEditable) { + return inputValue; + } + + if (!inputValue) { + // Reset in case user had typed something previously. + modelCtrl.$setValidity('editable', true); + return null; + } + + modelCtrl.$setValidity('editable', false); + return undefined; + }); + + modelCtrl.$formatters.push(function(modelValue) { + var candidateViewValue, emptyViewValue; + var locals = {}; + + // The validity may be set to false via $parsers (see above) if + // the model is restricted to selected values. If the model + // is set manually it is considered to be valid. + if (!isEditable) { + modelCtrl.$setValidity('editable', true); + } + + if (inputFormatter) { + locals.$model = modelValue; + return inputFormatter(originalScope, locals); + } + + //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; + }); + }; + + function extractOptions(ngModelCtrl) { + var ngModelOptions; + + if (angular.version.minor < 6) { // in angular < 1.6 $options could be missing + // guarantee a value + ngModelOptions = ngModelCtrl.$options || {}; + + // mimic 1.6+ api + ngModelOptions.getOption = function (key) { + return ngModelOptions[key]; + }; + } else { // in angular >=1.6 $options is always present + ngModelOptions = ngModelCtrl.$options; + } + + return ngModelOptions; + } + }]) + + .directive('uibTypeahead', function() { + return { + controller: 'UibTypeaheadController', + require: ['ngModel', 'uibTypeahead'], + link: function(originalScope, element, attrs, ctrls) { + ctrls[1].init(ctrls[0]); + } + }; + }) + + .directive('uibTypeaheadPopup', ['$$debounce', function($$debounce) { + return { + scope: { + matches: '=', + query: '=', + active: '=', + position: '&', + moveInProgress: '=', + select: '&', + assignIsOpen: '&', + debounce: '&' + }, + replace: true, + templateUrl: function(element, attrs) { + return attrs.popupTemplateUrl || 'uib/template/typeahead/typeahead-popup.html'; + }, + link: function(scope, element, attrs) { + scope.templateUrl = attrs.templateUrl; + + scope.isOpen = function() { + var isDropdownOpen = scope.matches.length > 0; + scope.assignIsOpen({ isOpen: isDropdownOpen }); + return isDropdownOpen; + }; + + scope.isActive = function(matchIdx) { + return scope.active === matchIdx; + }; + + scope.selectActive = function(matchIdx) { + scope.active = matchIdx; + }; + + scope.selectMatch = function(activeIdx, evt) { + var debounce = scope.debounce(); + if (angular.isNumber(debounce) || angular.isObject(debounce)) { + $$debounce(function() { + scope.select({activeIdx: activeIdx, evt: evt}); + }, angular.isNumber(debounce) ? debounce : debounce['default']); + } else { + scope.select({activeIdx: activeIdx, evt: evt}); + } + }; + } + }; + }]) + + .directive('uibTypeaheadMatch', ['$templateRequest', '$compile', '$parse', function($templateRequest, $compile, $parse) { + return { + scope: { + index: '=', + match: '=', + query: '=' + }, + link: function(scope, element, attrs) { + var tplUrl = $parse(attrs.templateUrl)(scope.$parent) || 'uib/template/typeahead/typeahead-match.html'; + $templateRequest(tplUrl).then(function(tplContent) { + var tplEl = angular.element(tplContent.trim()); + element.replaceWith(tplEl); + $compile(tplEl)(scope); + }); + } + }; + }]) + + .filter('uibTypeaheadHighlight', ['$sce', '$injector', '$log', function($sce, $injector, $log) { + var isSanitizePresent; + isSanitizePresent = $injector.has('$sanitize'); + + function escapeRegexp(queryToEscape) { + // Regex: capture the whole query string and replace it with the string that will be used to match + // the results, for example if the capture is "a" the result will be \a + return queryToEscape.replace(/([.?*+^$[\]\\(){}|-])/g, '\\$1'); + } + + function containsHtml(matchItem) { + return /<.*>/g.test(matchItem); + } + + return function(matchItem, query) { + if (!isSanitizePresent && containsHtml(matchItem)) { + $log.warn('Unsafe use of typeahead please use ngSanitize'); // Warn the user about the danger + } + matchItem = query ? ('' + matchItem).replace(new RegExp(escapeRegexp(query), 'gi'), '$&') : matchItem; // Replaces the capture string with a the same string inside of a "strong" tag + if (!isSanitizePresent) { + matchItem = $sce.trustAsHtml(matchItem); // If $sanitize is not present we pack the string in a $sce object for the ng-bind-html directive + } + return matchItem; + }; + }]); + +angular.module("uib/template/accordion/accordion-group.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("uib/template/accordion/accordion-group.html", + "
      \n" + + "

      \n" + + " {{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("uib/template/accordion/accordion.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("uib/template/accordion/accordion.html", + "
      "); }]); -angular.module("template/alert/alert.html", []).run(["$templateCache", function($templateCache) { - $templateCache.put("template/alert/alert.html", - "
      \n" + - " \n" + - "
      \n" + - "
      \n" + - ""); +angular.module("uib/template/alert/alert.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("uib/template/alert/alert.html", + "\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("uib/template/carousel/carousel.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("uib/template/carousel/carousel.html", + "
      \n" + + " 1\">\n" + + " \n" + + " previous\n" + + "\n" + + " 1\">\n" + + " \n" + + " next\n" + + "\n" + + "
        1\">\n" + + "
      1. \n" + + " slide {{ $index + 1 }} of {{ slides.length }}, currently active\n" + + "
      2. \n" + + "
      \n" + + ""); }]); -angular.module("template/carousel/slide.html", []).run(["$templateCache", function($templateCache) { - $templateCache.put("template/carousel/slide.html", - "
      \n" + - ""); +angular.module("uib/template/carousel/slide.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("uib/template/carousel/slide.html", + "
      \n" + + ""); }]); -angular.module("template/datepicker/datepicker.html", []).run(["$templateCache", function($templateCache) { - $templateCache.put("template/datepicker/datepicker.html", - "\n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " 0\" class=\"h6\">\n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - "
      #{{label}}
      {{ getWeekNumber(row) }}\n" + - " \n" + - "
      \n" + - ""); +angular.module("uib/template/datepicker/datepicker.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("uib/template/datepicker/datepicker.html", + "
      \n" + + "
      \n" + + "
      \n" + + "
      \n" + + "
      \n" + + ""); }]); -angular.module("template/datepicker/popup.html", []).run(["$templateCache", function($templateCache) { - $templateCache.put("template/datepicker/popup.html", - "
        \n" + - "
      • \n" + - "
      • \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - "
      • \n" + - "
      \n" + - ""); +angular.module("uib/template/datepicker/day.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("uib/template/datepicker/day.html", + "\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + "
      {{::label.abbr}}
      {{ weekNumbers[$index] }}\n" + + " \n" + + "
      \n" + + ""); }]); -angular.module("template/modal/backdrop.html", []).run(["$templateCache", function($templateCache) { - $templateCache.put("template/modal/backdrop.html", - "
      "); +angular.module("uib/template/datepicker/month.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("uib/template/datepicker/month.html", + "\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + "
      \n" + + " \n" + + "
      \n" + + ""); }]); -angular.module("template/modal/window.html", []).run(["$templateCache", function($templateCache) { - $templateCache.put("template/modal/window.html", - "
      \n" + - "
      \n" + - "
      "); +angular.module("uib/template/datepicker/year.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("uib/template/datepicker/year.html", + "\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + "
      \n" + + " \n" + + "
      \n" + + ""); }]); -angular.module("template/pagination/pager.html", []).run(["$templateCache", function($templateCache) { - $templateCache.put("template/pagination/pager.html", - ""); +angular.module("uib/template/datepickerPopup/popup.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("uib/template/datepickerPopup/popup.html", + "
        \n" + + "
      • \n" + + "
      • \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + "
      • \n" + + "
      \n" + + ""); }]); -angular.module("template/pagination/pagination.html", []).run(["$templateCache", function($templateCache) { - $templateCache.put("template/pagination/pagination.html", - ""); +angular.module("uib/template/modal/window.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("uib/template/modal/window.html", + "
      \n" + + ""); }]); -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("uib/template/pager/pager.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("uib/template/pager/pager.html", + "
    3. {{::getText('previous')}}
    4. \n" + + "
    5. {{::getText('next')}}
    6. \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("uib/template/pagination/pagination.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("uib/template/pagination/pagination.html", + "
    7. {{::getText('first')}}
    8. \n" + + "
    9. {{::getText('previous')}}
    10. \n" + + "
    11. {{page.text}}
    12. \n" + + "
    13. {{::getText('next')}}
    14. \n" + + "
    15. {{::getText('last')}}
    16. \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("uib/template/tooltip/tooltip-html-popup.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("uib/template/tooltip/tooltip-html-popup.html", + "
      \n" + + "
      \n" + + ""); }]); -angular.module("template/progressbar/bar.html", []).run(["$templateCache", function($templateCache) { - $templateCache.put("template/progressbar/bar.html", - "
      "); +angular.module("uib/template/tooltip/tooltip-popup.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("uib/template/tooltip/tooltip-popup.html", + "
      \n" + + "
      \n" + + ""); }]); -angular.module("template/progressbar/progress.html", []).run(["$templateCache", function($templateCache) { - $templateCache.put("template/progressbar/progress.html", - "
      "); +angular.module("uib/template/tooltip/tooltip-template-popup.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("uib/template/tooltip/tooltip-template-popup.html", + "
      \n" + + "
      \n" + + ""); }]); -angular.module("template/progressbar/progressbar.html", []).run(["$templateCache", function($templateCache) { - $templateCache.put("template/progressbar/progressbar.html", - "
      "); +angular.module("uib/template/popover/popover-html.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("uib/template/popover/popover-html.html", + "
      \n" + + "\n" + + "
      \n" + + "

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

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

      \n" + + "
      \n" + + "
      \n" + + ""); }]); -angular.module("template/tabs/tabset-titles.html", []).run(["$templateCache", function($templateCache) { - $templateCache.put("template/tabs/tabset-titles.html", - "
        \n" + - "
      \n" + - ""); +angular.module("uib/template/progressbar/bar.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("uib/template/progressbar/bar.html", + "
      \n" + + ""); }]); -angular.module("template/tabs/tabset.html", []).run(["$templateCache", function($templateCache) { - $templateCache.put("template/tabs/tabset.html", - "\n" + - "
      \n" + - "
        \n" + - "
        \n" + - "
        \n" + - "
        \n" + - "
        \n" + - "
        \n" + - ""); +angular.module("uib/template/progressbar/progress.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("uib/template/progressbar/progress.html", + "
        "); }]); -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("uib/template/progressbar/progressbar.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("uib/template/progressbar/progressbar.html", + "
        \n" + + "
        \n" + + "
        \n" + + ""); }]); -angular.module("template/typeahead/typeahead-match.html", []).run(["$templateCache", function($templateCache) { - $templateCache.put("template/typeahead/typeahead-match.html", - ""); +angular.module("uib/template/rating/rating.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("uib/template/rating/rating.html", + "\n" + + " ({{ $index < value ? '*' : ' ' }})\n" + + " \n" + + "\n" + + ""); }]); -angular.module("template/typeahead/typeahead-popup.html", []).run(["$templateCache", function($templateCache) { - $templateCache.put("template/typeahead/typeahead-popup.html", - "
          \n" + - "
        • \n" + - "
          \n" + - "
        • \n" + - "
        "); +angular.module("uib/template/tabs/tab.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("uib/template/tabs/tab.html", + "
      • \n" + + " {{heading}}\n" + + "
      • \n" + + ""); }]); +angular.module("uib/template/tabs/tabset.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("uib/template/tabs/tabset.html", + "
        \n" + + "
          \n" + + "
          \n" + + "
          \n" + + "
          \n" + + "
          \n" + + "
          \n" + + ""); +}]); + +angular.module("uib/template/timepicker/timepicker.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("uib/template/timepicker/timepicker.html", + "\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + "
            
          \n" + + " \n" + + " :\n" + + " \n" + + " :\n" + + " \n" + + "
            
          \n" + + ""); +}]); + +angular.module("uib/template/typeahead/typeahead-match.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("uib/template/typeahead/typeahead-match.html", + "\n" + + ""); +}]); + +angular.module("uib/template/typeahead/typeahead-popup.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("uib/template/typeahead/typeahead-popup.html", + "
            \n" + + "
          • \n" + + "
            \n" + + "
          • \n" + + "
          \n" + + ""); +}]); +angular.module('ui.bootstrap.carousel').run(function() {!angular.$$csp().noInlineStyle && !angular.$$uibCarouselCss && angular.element(document).find('head').prepend(''); angular.$$uibCarouselCss = true; }); +angular.module('ui.bootstrap.datepicker').run(function() {!angular.$$csp().noInlineStyle && !angular.$$uibDatepickerCss && angular.element(document).find('head').prepend(''); angular.$$uibDatepickerCss = true; }); +angular.module('ui.bootstrap.position').run(function() {!angular.$$csp().noInlineStyle && !angular.$$uibPositionCss && angular.element(document).find('head').prepend(''); angular.$$uibPositionCss = true; }); +angular.module('ui.bootstrap.datepickerPopup').run(function() {!angular.$$csp().noInlineStyle && !angular.$$uibDatepickerpopupCss && angular.element(document).find('head').prepend(''); angular.$$uibDatepickerpopupCss = true; }); +angular.module('ui.bootstrap.tooltip').run(function() {!angular.$$csp().noInlineStyle && !angular.$$uibTooltipCss && angular.element(document).find('head').prepend(''); angular.$$uibTooltipCss = true; }); +angular.module('ui.bootstrap.timepicker').run(function() {!angular.$$csp().noInlineStyle && !angular.$$uibTimepickerCss && angular.element(document).find('head').prepend(''); angular.$$uibTimepickerCss = true; }); +angular.module('ui.bootstrap.typeahead').run(function() {!angular.$$csp().noInlineStyle && !angular.$$uibTypeaheadCss && angular.element(document).find('head').prepend(''); angular.$$uibTypeaheadCss = true; }); + var dialogModule = angular.module("dialog", [ 'ui.bootstrap' ]); -dialogModule.factory('$dialog', ['$rootScope', '$modal', '$sce', function($rootScope, $modal, $sce) { +dialogModule.factory('$dialog', ['$rootScope', '$uibModal', '$sce', function($rootScope, $uibModal, $sce) { function dialog(modalOptions, resultFn) { - var dialog = $modal.open(modalOptions); + var dialog = $uibModal.open(modalOptions); if (resultFn) dialog.result.then(resultFn); dialog.values = modalOptions; return dialog; @@ -3641,12 +7818,12 @@ dialogModule.factory('$dialog', ['$rootScope', '$modal', '$sce', function($rootS dialogModule.run(["$templateCache", function($templateCache) { $templateCache.put("template/messageBox/message.html", - '\n' + - '\n' + - '\n'); + '\n' + + '\n' + + '\n'); }]); -dialogModule.controller('MessageBoxController', ['$scope', '$modalInstance', function ($scope, $modalInstance) { - $scope.close = function (result) { $modalInstance.close(result); } +dialogModule.controller('MessageBoxController', ['$scope', '$uibModalInstance', function ($scope, $uibModalInstance) { + $scope.close = function (result) { $uibModalInstance.close(result); } }]); \ No newline at end of file diff --git a/docs-web/src/main/webapp/src/lib/angular.ui-router.js b/docs-web/src/main/webapp/src/lib/angular.ui-router.js index 44908f40..9880b681 100644 --- a/docs-web/src/main/webapp/src/lib/angular.ui-router.js +++ b/docs-web/src/main/webapp/src/lib/angular.ui-router.js @@ -1,44 +1,636 @@ /** - * State-based routing for AngularJS - * @version v0.2.7 - * @link http://angular-ui.github.com/ + * State-based routing for AngularJS 1.x + * NOTICE: This monolithic bundle also bundles the @uirouter/core code. + * This causes it to be incompatible with plugins that depend on @uirouter/core. + * We recommend switching to the ui-router-core.js and ui-router-angularjs.js bundles instead. + * For more information, see https://ui-router.github.io/blog/uirouter-for-angularjs-umd-bundles + * @version v1.0.10 + * @link https://ui-router.github.io * @license MIT License, http://www.opensource.org/licenses/MIT */ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('angular')) : + typeof define === 'function' && define.amd ? define(['exports', 'angular'], factory) : + (factory((global['@uirouter/angularjs'] = {}),global.angular)); +}(this, (function (exports,ng_from_import) { 'use strict'; -/* commonjs package manager support (eg componentjs) */ -if (typeof module !== "undefined" && typeof exports !== "undefined" && module.exports === exports){ - module.exports = 'ui.router'; -} + var ng_from_global = angular; + var ng = (ng_from_import && ng_from_import.module) ? ng_from_import : ng_from_global; -(function (window, angular, undefined) { - /*jshint globalstrict:true*/ - /*global angular:false*/ - 'use strict'; - - var isDefined = angular.isDefined, - isFunction = angular.isFunction, - isString = angular.isString, - isObject = angular.isObject, - isArray = angular.isArray, - forEach = angular.forEach, - extend = angular.extend, - copy = angular.copy; - - function inherit(parent, extra) { - return extend(new (extend(function() {}, { prototype: parent }))(), extra); + /** + * Higher order functions + * + * These utility functions are exported, but are subject to change without notice. + * + * @module common_hof + */ /** */ + /** + * Returns a new function for [Partial Application](https://en.wikipedia.org/wiki/Partial_application) of the original function. + * + * Given a function with N parameters, returns a new function that supports partial application. + * The new function accepts anywhere from 1 to N parameters. When that function is called with M parameters, + * where M is less than N, it returns a new function that accepts the remaining parameters. It continues to + * accept more parameters until all N parameters have been supplied. + * + * + * This contrived example uses a partially applied function as an predicate, which returns true + * if an object is found in both arrays. + * @example + * ``` + * // returns true if an object is in both of the two arrays + * function inBoth(array1, array2, object) { + * return array1.indexOf(object) !== -1 && + * array2.indexOf(object) !== 1; + * } + * let obj1, obj2, obj3, obj4, obj5, obj6, obj7 + * let foos = [obj1, obj3] + * let bars = [obj3, obj4, obj5] + * + * // A curried "copy" of inBoth + * let curriedInBoth = curry(inBoth); + * // Partially apply both the array1 and array2 + * let inFoosAndBars = curriedInBoth(foos, bars); + * + * // Supply the final argument; since all arguments are + * // supplied, the original inBoth function is then called. + * let obj1InBoth = inFoosAndBars(obj1); // false + * + * // Use the inFoosAndBars as a predicate. + * // Filter, on each iteration, supplies the final argument + * let allObjs = [ obj1, obj2, obj3, obj4, obj5, obj6, obj7 ]; + * let foundInBoth = allObjs.filter(inFoosAndBars); // [ obj3 ] + * + * ``` + * + * Stolen from: http://stackoverflow.com/questions/4394747/javascript-curry-function + * + * @param fn + * @returns {*|function(): (*|any)} + */ + function curry(fn) { + var initial_args = [].slice.apply(arguments, [1]); + var func_args_length = fn.length; + function curried(args) { + if (args.length >= func_args_length) + return fn.apply(null, args); + return function () { + return curried(args.concat([].slice.apply(arguments))); + }; + } + return curried(initial_args); } - - function merge(dst) { - forEach(arguments, function(obj) { - if (obj !== dst) { - forEach(obj, function(value, key) { - if (!dst.hasOwnProperty(key)) dst[key] = value; - }); + /** + * Given a varargs list of functions, returns a function that composes the argument functions, right-to-left + * given: f(x), g(x), h(x) + * let composed = compose(f,g,h) + * then, composed is: f(g(h(x))) + */ + function compose() { + var args = arguments; + var start = args.length - 1; + return function () { + var i = start, result = args[start].apply(this, arguments); + while (i--) + result = args[i].call(this, result); + return result; + }; + } + /** + * Given a varargs list of functions, returns a function that is composes the argument functions, left-to-right + * given: f(x), g(x), h(x) + * let piped = pipe(f,g,h); + * then, piped is: h(g(f(x))) + */ + function pipe() { + var funcs = []; + for (var _i = 0; _i < arguments.length; _i++) { + funcs[_i] = arguments[_i]; + } + return compose.apply(null, [].slice.call(arguments).reverse()); + } + /** + * Given a property name, returns a function that returns that property from an object + * let obj = { foo: 1, name: "blarg" }; + * let getName = prop("name"); + * getName(obj) === "blarg" + */ + var prop = function (name) { + return function (obj) { return obj && obj[name]; }; + }; + /** + * Given a property name and a value, returns a function that returns a boolean based on whether + * the passed object has a property that matches the value + * let obj = { foo: 1, name: "blarg" }; + * let getName = propEq("name", "blarg"); + * getName(obj) === true + */ + var propEq = curry(function (name, val, obj) { return obj && obj[name] === val; }); + /** + * Given a dotted property name, returns a function that returns a nested property from an object, or undefined + * let obj = { id: 1, nestedObj: { foo: 1, name: "blarg" }, }; + * let getName = prop("nestedObj.name"); + * getName(obj) === "blarg" + * let propNotFound = prop("this.property.doesnt.exist"); + * propNotFound(obj) === undefined + */ + var parse = function (name) { + return pipe.apply(null, name.split(".").map(prop)); + }; + /** + * Given a function that returns a truthy or falsey value, returns a + * function that returns the opposite (falsey or truthy) value given the same inputs + */ + var not = function (fn) { + return function () { + var args = []; + for (var _i = 0; _i < arguments.length; _i++) { + args[_i] = arguments[_i]; } - }); - return dst; + return !fn.apply(null, args); + }; + }; + /** + * Given two functions that return truthy or falsey values, returns a function that returns truthy + * if both functions return truthy for the given arguments + */ + function and(fn1, fn2) { + return function () { + var args = []; + for (var _i = 0; _i < arguments.length; _i++) { + args[_i] = arguments[_i]; + } + return fn1.apply(null, args) && fn2.apply(null, args); + }; + } + /** + * Given two functions that return truthy or falsey values, returns a function that returns truthy + * if at least one of the functions returns truthy for the given arguments + */ + function or(fn1, fn2) { + return function () { + var args = []; + for (var _i = 0; _i < arguments.length; _i++) { + args[_i] = arguments[_i]; + } + return fn1.apply(null, args) || fn2.apply(null, args); + }; + } + /** + * Check if all the elements of an array match a predicate function + * + * @param fn1 a predicate function `fn1` + * @returns a function which takes an array and returns true if `fn1` is true for all elements of the array + */ + var all = function (fn1) { + return function (arr) { return arr.reduce(function (b, x) { return b && !!fn1(x); }, true); }; + }; + var any = function (fn1) { + return function (arr) { return arr.reduce(function (b, x) { return b || !!fn1(x); }, false); }; + }; + /** Given a class, returns a Predicate function that returns true if the object is of that class */ + var is = function (ctor) { + return function (obj) { + return (obj != null && obj.constructor === ctor || obj instanceof ctor); + }; + }; + /** Given a value, returns a Predicate function that returns true if another value is === equal to the original value */ + var eq = function (val) { return function (other) { + return val === other; + }; }; + /** Given a value, returns a function which returns the value */ + var val = function (v) { return function () { return v; }; }; + function invoke(fnName, args) { + return function (obj) { + return obj[fnName].apply(obj, args); + }; + } + /** + * Sorta like Pattern Matching (a functional programming conditional construct) + * + * See http://c2.com/cgi/wiki?PatternMatching + * + * This is a conditional construct which allows a series of predicates and output functions + * to be checked and then applied. Each predicate receives the input. If the predicate + * returns truthy, then its matching output function (mapping function) is provided with + * the input and, then the result is returned. + * + * Each combination (2-tuple) of predicate + output function should be placed in an array + * of size 2: [ predicate, mapFn ] + * + * These 2-tuples should be put in an outer array. + * + * @example + * ``` + * + * // Here's a 2-tuple where the first element is the isString predicate + * // and the second element is a function that returns a description of the input + * let firstTuple = [ angular.isString, (input) => `Heres your string ${input}` ]; + * + * // Second tuple: predicate "isNumber", mapfn returns a description + * let secondTuple = [ angular.isNumber, (input) => `(${input}) That's a number!` ]; + * + * let third = [ (input) => input === null, (input) => `Oh, null...` ]; + * + * let fourth = [ (input) => input === undefined, (input) => `notdefined` ]; + * + * let descriptionOf = pattern([ firstTuple, secondTuple, third, fourth ]); + * + * console.log(descriptionOf(undefined)); // 'notdefined' + * console.log(descriptionOf(55)); // '(55) That's a number!' + * console.log(descriptionOf("foo")); // 'Here's your string foo' + * ``` + * + * @param struct A 2D array. Each element of the array should be an array, a 2-tuple, + * with a Predicate and a mapping/output function + * @returns {function(any): *} + */ + function pattern(struct) { + return function (x) { + for (var i = 0; i < struct.length; i++) { + if (struct[i][0](x)) + return struct[i][1](x); + } + }; } + /** + * @coreapi + * @module core + */ + /** + * Matches state names using glob-like pattern strings. + * + * Globs can be used in specific APIs including: + * + * - [[StateService.is]] + * - [[StateService.includes]] + * - The first argument to Hook Registration functions like [[TransitionService.onStart]] + * - [[HookMatchCriteria]] and [[HookMatchCriterion]] + * + * A `Glob` string is a pattern which matches state names. + * Nested state names are split into segments (separated by a dot) when processing. + * The state named `foo.bar.baz` is split into three segments ['foo', 'bar', 'baz'] + * + * Globs work according to the following rules: + * + * ### Exact match: + * + * The glob `'A.B'` matches the state named exactly `'A.B'`. + * + * | Glob |Matches states named|Does not match state named| + * |:------------|:--------------------|:---------------------| + * | `'A'` | `'A'` | `'B'` , `'A.C'` | + * | `'A.B'` | `'A.B'` | `'A'` , `'A.B.C'` | + * | `'foo'` | `'foo'` | `'FOO'` , `'foo.bar'`| + * + * ### Single star (`*`) + * + * A single star (`*`) is a wildcard that matches exactly one segment. + * + * | Glob |Matches states named |Does not match state named | + * |:------------|:---------------------|:--------------------------| + * | `'*'` | `'A'` , `'Z'` | `'A.B'` , `'Z.Y.X'` | + * | `'A.*'` | `'A.B'` , `'A.C'` | `'A'` , `'A.B.C'` | + * | `'A.*.*'` | `'A.B.C'` , `'A.X.Y'`| `'A'`, `'A.B'` , `'Z.Y.X'`| + * + * ### Double star (`**`) + * + * A double star (`'**'`) is a wildcard that matches *zero or more segments* + * + * | Glob |Matches states named |Does not match state named | + * |:------------|:----------------------------------------------|:----------------------------------| + * | `'**'` | `'A'` , `'A.B'`, `'Z.Y.X'` | (matches all states) | + * | `'A.**'` | `'A'` , `'A.B'` , `'A.C.X'` | `'Z.Y.X'` | + * | `'**.X'` | `'X'` , `'A.X'` , `'Z.Y.X'` | `'A'` , `'A.login.Z'` | + * | `'A.**.X'` | `'A.X'` , `'A.B.X'` , `'A.B.C.X'` | `'A'` , `'A.B.C'` | + * + */ + var Glob = /** @class */ (function () { + function Glob(text) { + this.text = text; + this.glob = text.split('.'); + var regexpString = this.text.split('.') + .map(function (seg) { + if (seg === '**') + return '(?:|(?:\\.[^.]*)*)'; + if (seg === '*') + return '\\.[^.]*'; + return '\\.' + seg; + }).join(''); + this.regexp = new RegExp("^" + regexpString + "$"); + } + Glob.prototype.matches = function (name) { + return this.regexp.test('.' + name); + }; + /** Returns true if the string has glob-like characters in it */ + Glob.is = function (text) { + return !!/[!,*]+/.exec(text); + }; + /** Returns a glob from the string, or null if the string isn't Glob-like */ + Glob.fromString = function (text) { + return Glob.is(text) ? new Glob(text) : null; + }; + return Glob; + }()); + + /** + * Internal representation of a UI-Router state. + * + * Instances of this class are created when a [[StateDeclaration]] is registered with the [[StateRegistry]]. + * + * A registered [[StateDeclaration]] is augmented with a getter ([[StateDeclaration.$$state]]) which returns the corresponding [[StateObject]] object. + * + * This class prototypally inherits from the corresponding [[StateDeclaration]]. + * Each of its own properties (i.e., `hasOwnProperty`) are built using builders from the [[StateBuilder]]. + */ + var StateObject = /** @class */ (function () { + /** @deprecated use State.create() */ + function StateObject(config) { + return StateObject.create(config || {}); + } + /** + * Create a state object to put the private/internal implementation details onto. + * The object's prototype chain looks like: + * (Internal State Object) -> (Copy of State.prototype) -> (State Declaration object) -> (State Declaration's prototype...) + * + * @param stateDecl the user-supplied State Declaration + * @returns {StateObject} an internal State object + */ + StateObject.create = function (stateDecl) { + stateDecl = StateObject.isStateClass(stateDecl) ? new stateDecl() : stateDecl; + var state = inherit(inherit(stateDecl, StateObject.prototype)); + stateDecl.$$state = function () { return state; }; + state.self = stateDecl; + state.__stateObjectCache = { + nameGlob: Glob.fromString(state.name) // might return null + }; + return state; + }; + /** + * Returns true if the provided parameter is the same state. + * + * Compares the identity of the state against the passed value, which is either an object + * reference to the actual `State` instance, the original definition object passed to + * `$stateProvider.state()`, or the fully-qualified name. + * + * @param ref Can be one of (a) a `State` instance, (b) an object that was passed + * into `$stateProvider.state()`, (c) the fully-qualified name of a state as a string. + * @returns Returns `true` if `ref` matches the current `State` instance. + */ + StateObject.prototype.is = function (ref) { + return this === ref || this.self === ref || this.fqn() === ref; + }; + /** + * @deprecated this does not properly handle dot notation + * @returns Returns a dot-separated name of the state. + */ + StateObject.prototype.fqn = function () { + if (!this.parent || !(this.parent instanceof this.constructor)) + return this.name; + var name = this.parent.fqn(); + return name ? name + "." + this.name : this.name; + }; + /** + * Returns the root node of this state's tree. + * + * @returns The root of this state's tree. + */ + StateObject.prototype.root = function () { + return this.parent && this.parent.root() || this; + }; + /** + * Gets the state's `Param` objects + * + * Gets the list of [[Param]] objects owned by the state. + * If `opts.inherit` is true, it also includes the ancestor states' [[Param]] objects. + * If `opts.matchingKeys` exists, returns only `Param`s whose `id` is a key on the `matchingKeys` object + * + * @param opts options + */ + StateObject.prototype.parameters = function (opts) { + opts = defaults(opts, { inherit: true, matchingKeys: null }); + var inherited = opts.inherit && this.parent && this.parent.parameters() || []; + return inherited.concat(values(this.params)) + .filter(function (param) { return !opts.matchingKeys || opts.matchingKeys.hasOwnProperty(param.id); }); + }; + /** + * Returns a single [[Param]] that is owned by the state + * + * If `opts.inherit` is true, it also searches the ancestor states` [[Param]]s. + * @param id the name of the [[Param]] to return + * @param opts options + */ + StateObject.prototype.parameter = function (id, opts) { + if (opts === void 0) { opts = {}; } + return (this.url && this.url.parameter(id, opts) || + find(values(this.params), propEq('id', id)) || + opts.inherit && this.parent && this.parent.parameter(id)); + }; + StateObject.prototype.toString = function () { + return this.fqn(); + }; + /** Predicate which returns true if the object is an class with @State() decorator */ + StateObject.isStateClass = function (stateDecl) { + return isFunction(stateDecl) && stateDecl['__uiRouterState'] === true; + }; + /** Predicate which returns true if the object is an internal [[StateObject]] object */ + StateObject.isState = function (obj) { + return isObject(obj['__stateObjectCache']); + }; + return StateObject; + }()); + + /** Predicates + * + * These predicates return true/false based on the input. + * Although these functions are exported, they are subject to change without notice. + * + * @module common_predicates + */ + /** */ + var toStr = Object.prototype.toString; + var tis = function (t) { return function (x) { return typeof (x) === t; }; }; + var isUndefined = tis('undefined'); + var isDefined = not(isUndefined); + var isNull = function (o) { return o === null; }; + var isNullOrUndefined = or(isNull, isUndefined); + var isFunction = tis('function'); + var isNumber = tis('number'); + var isString = tis('string'); + var isObject = function (x) { return x !== null && typeof x === 'object'; }; + var isArray = Array.isArray; + var isDate = (function (x) { return toStr.call(x) === '[object Date]'; }); + var isRegExp = (function (x) { return toStr.call(x) === '[object RegExp]'; }); + var isState = StateObject.isState; + /** + * Predicate which checks if a value is injectable + * + * A value is "injectable" if it is a function, or if it is an ng1 array-notation-style array + * where all the elements in the array are Strings, except the last one, which is a Function + */ + function isInjectable(val$$1) { + if (isArray(val$$1) && val$$1.length) { + var head = val$$1.slice(0, -1), tail = val$$1.slice(-1); + return !(head.filter(not(isString)).length || tail.filter(not(isFunction)).length); + } + return isFunction(val$$1); + } + /** + * Predicate which checks if a value looks like a Promise + * + * It is probably a Promise if it's an object, and it has a `then` property which is a Function + */ + var isPromise = and(isObject, pipe(prop('then'), isFunction)); + + var notImplemented = function (fnname) { return function () { + throw new Error(fnname + "(): No coreservices implementation for UI-Router is loaded."); + }; }; + var services = { + $q: undefined, + $injector: undefined, + }; + + /** + * Random utility functions used in the UI-Router code + * + * These functions are exported, but are subject to change without notice. + * + * @preferred + * @module common + */ + /** for typedoc */ + var root = (typeof self === 'object' && self.self === self && self) || + (typeof global === 'object' && global.global === global && global) || undefined; + var angular$1 = root.angular || {}; + var fromJson = angular$1.fromJson || JSON.parse.bind(JSON); + var toJson = angular$1.toJson || JSON.stringify.bind(JSON); + var forEach = angular$1.forEach || _forEach; + var extend = Object.assign || _extend; + var equals = angular$1.equals || _equals; + function identity(x) { return x; } + function noop$1() { } + /** + * Builds proxy functions on the `to` object which pass through to the `from` object. + * + * For each key in `fnNames`, creates a proxy function on the `to` object. + * The proxy function calls the real function on the `from` object. + * + * + * #### Example: + * This example creates an new class instance whose functions are prebound to the new'd object. + * ```js + * class Foo { + * constructor(data) { + * // Binds all functions from Foo.prototype to 'this', + * // then copies them to 'this' + * bindFunctions(Foo.prototype, this, this); + * this.data = data; + * } + * + * log() { + * console.log(this.data); + * } + * } + * + * let myFoo = new Foo([1,2,3]); + * var logit = myFoo.log; + * logit(); // logs [1, 2, 3] from the myFoo 'this' instance + * ``` + * + * #### Example: + * This example creates a bound version of a service function, and copies it to another object + * ``` + * + * var SomeService = { + * this.data = [3, 4, 5]; + * this.log = function() { + * console.log(this.data); + * } + * } + * + * // Constructor fn + * function OtherThing() { + * // Binds all functions from SomeService to SomeService, + * // then copies them to 'this' + * bindFunctions(SomeService, this, SomeService); + * } + * + * let myOtherThing = new OtherThing(); + * myOtherThing.log(); // logs [3, 4, 5] from SomeService's 'this' + * ``` + * + * @param source A function that returns the source object which contains the original functions to be bound + * @param target A function that returns the target object which will receive the bound functions + * @param bind A function that returns the object which the functions will be bound to + * @param fnNames The function names which will be bound (Defaults to all the functions found on the 'from' object) + * @param latebind If true, the binding of the function is delayed until the first time it's invoked + */ + function createProxyFunctions(source, target, bind, fnNames, latebind) { + if (latebind === void 0) { latebind = false; } + var bindFunction = function (fnName) { + return source()[fnName].bind(bind()); + }; + var makeLateRebindFn = function (fnName) { return function lateRebindFunction() { + target[fnName] = bindFunction(fnName); + return target[fnName].apply(null, arguments); + }; }; + fnNames = fnNames || Object.keys(source()); + return fnNames.reduce(function (acc, name) { + acc[name] = latebind ? makeLateRebindFn(name) : bindFunction(name); + return acc; + }, target); + } + /** + * prototypal inheritance helper. + * Creates a new object which has `parent` object as its prototype, and then copies the properties from `extra` onto it + */ + var inherit = function (parent, extra) { + return extend(Object.create(parent), extra); + }; + /** Given an array, returns true if the object is found in the array, (using indexOf) */ + var inArray = curry(_inArray); + function _inArray(array, obj) { + return array.indexOf(obj) !== -1; + } + /** + * Given an array, and an item, if the item is found in the array, it removes it (in-place). + * The same array is returned + */ + var removeFrom = curry(_removeFrom); + function _removeFrom(array, obj) { + var idx = array.indexOf(obj); + if (idx >= 0) + array.splice(idx, 1); + return array; + } + /** pushes a values to an array and returns the value */ + var pushTo = curry(_pushTo); + function _pushTo(arr, val$$1) { + return (arr.push(val$$1), val$$1); + } + /** Given an array of (deregistration) functions, calls all functions and removes each one from the source array */ + var deregAll = function (functions) { + return functions.slice().forEach(function (fn) { + typeof fn === 'function' && fn(); + removeFrom(functions, fn); + }); + }; + /** + * Applies a set of defaults to an options object. The options object is filtered + * to only those properties of the objects in the defaultsList. + * Earlier objects in the defaultsList take precedence when applying defaults. + */ + function defaults(opts) { + var defaultsList = []; + for (var _i = 1; _i < arguments.length; _i++) { + defaultsList[_i - 1] = arguments[_i]; + } + var _defaultsList = defaultsList.concat({}).reverse(); + var defaultVals = extend.apply(null, _defaultsList); + return extend({}, defaultVals, pick(opts || {}, Object.keys(defaultVals))); + } + /** Reduce function that merges each element of the list into a single object, using extend */ + var mergeR = function (memo, item) { return extend(memo, item); }; /** * Finds the common ancestor path between two states. * @@ -48,1722 +640,9435 @@ if (typeof module !== "undefined" && typeof exports !== "undefined" && module.ex */ function ancestors(first, second) { var path = []; - for (var n in first.path) { - if (first.path[n] === "") continue; - if (!second.path[n]) break; + if (first.path[n] !== second.path[n]) + break; path.push(first.path[n]); } return path; } - /** - * IE8-safe wrapper for `Object.keys()`. + * Return a copy of the object only containing the whitelisted properties. * - * @param {Object} object A JavaScript object. - * @return {Array} Returns the keys of the object as an array. + * #### Example: + * ``` + * var foo = { a: 1, b: 2, c: 3 }; + * var ab = pick(foo, ['a', 'b']); // { a: 1, b: 2 } + * ``` + * @param obj the source object + * @param propNames an Array of strings, which are the whitelisted property names */ - function keys(object) { - if (Object.keys) { - return Object.keys(object); + function pick(obj, propNames) { + var objCopy = {}; + for (var prop_1 in obj) { + if (propNames.indexOf(prop_1) !== -1) { + objCopy[prop_1] = obj[prop_1]; + } } - var result = []; - - angular.forEach(object, function(val, key) { - result.push(key); + return objCopy; + } + /** + * Return a copy of the object omitting the blacklisted properties. + * + * @example + * ``` + * + * var foo = { a: 1, b: 2, c: 3 }; + * var ab = omit(foo, ['a', 'b']); // { c: 3 } + * ``` + * @param obj the source object + * @param propNames an Array of strings, which are the blacklisted property names + */ + function omit(obj, propNames) { + return Object.keys(obj) + .filter(not(inArray(propNames))) + .reduce(function (acc, key) { return (acc[key] = obj[key], acc); }, {}); + } + /** + * Maps an array, or object to a property (by name) + */ + function pluck(collection, propName) { + return map(collection, prop(propName)); + } + /** Filters an Array or an Object's properties based on a predicate */ + function filter(collection, callback) { + var arr = isArray(collection), result = arr ? [] : {}; + var accept = arr ? function (x) { return result.push(x); } : function (x, key) { return result[key] = x; }; + forEach(collection, function (item, i) { + if (callback(item, i)) + accept(item, i); }); return result; } - - /** - * IE8-safe wrapper for `Array.prototype.indexOf()`. - * - * @param {Array} array A JavaScript array. - * @param {*} value A value to search the array for. - * @return {Number} Returns the array index value of `value`, or `-1` if not present. - */ - function arraySearch(array, value) { - if (Array.prototype.indexOf) { - return array.indexOf(value, Number(arguments[2]) || 0); - } - var len = array.length >>> 0, from = Number(arguments[2]) || 0; - from = (from < 0) ? Math.ceil(from) : Math.floor(from); - - if (from < 0) from += len; - - for (; from < len; from++) { - if (from in array && array[from] === value) return from; - } - return -1; + /** Finds an object from an array, or a property of an object, that matches a predicate */ + function find(collection, callback) { + var result; + forEach(collection, function (item, i) { + if (result) + return; + if (callback(item, i)) + result = item; + }); + return result; + } + /** Given an object, returns a new object, where each property is transformed by the callback function */ + var mapObj = map; + /** Maps an array or object properties using a callback function */ + function map(collection, callback) { + var result = isArray(collection) ? [] : {}; + forEach(collection, function (item, i) { return result[i] = callback(item, i); }); + return result; } - /** - * Merges a set of parameters with all parameters inherited between the common parents of the - * current state and a given destination state. + * Given an object, return its enumerable property values * - * @param {Object} currentParams The value of the current state parameters ($stateParams). - * @param {Object} newParams The set of parameters which will be composited with inherited params. - * @param {Object} $current Internal definition of object representing the current state. - * @param {Object} $to Internal definition of object representing state to transition to. + * @example + * ``` + * + * let foo = { a: 1, b: 2, c: 3 } + * let vals = values(foo); // [ 1, 2, 3 ] + * ``` */ - function inheritParams(currentParams, newParams, $current, $to) { - var parents = ancestors($current, $to), parentParams, inherited = {}, inheritList = []; - - for (var i in parents) { - if (!parents[i].params || !parents[i].params.length) continue; - parentParams = parents[i].params; - - for (var j in parentParams) { - if (arraySearch(inheritList, parentParams[j]) >= 0) continue; - inheritList.push(parentParams[j]); - inherited[parentParams[j]] = currentParams[parentParams[j]]; + var values = function (obj) { + return Object.keys(obj).map(function (key) { return obj[key]; }); + }; + /** + * Reduce function that returns true if all of the values are truthy. + * + * @example + * ``` + * + * let vals = [ 1, true, {}, "hello world"]; + * vals.reduce(allTrueR, true); // true + * + * vals.push(0); + * vals.reduce(allTrueR, true); // false + * ``` + */ + var allTrueR = function (memo, elem) { return memo && elem; }; + /** + * Reduce function that returns true if any of the values are truthy. + * + * * @example + * ``` + * + * let vals = [ 0, null, undefined ]; + * vals.reduce(anyTrueR, true); // false + * + * vals.push("hello world"); + * vals.reduce(anyTrueR, true); // true + * ``` + */ + var anyTrueR = function (memo, elem) { return memo || elem; }; + /** + * Reduce function which un-nests a single level of arrays + * @example + * ``` + * + * let input = [ [ "a", "b" ], [ "c", "d" ], [ [ "double", "nested" ] ] ]; + * input.reduce(unnestR, []) // [ "a", "b", "c", "d", [ "double, "nested" ] ] + * ``` + */ + var unnestR = function (memo, elem) { return memo.concat(elem); }; + /** + * Reduce function which recursively un-nests all arrays + * + * @example + * ``` + * + * let input = [ [ "a", "b" ], [ "c", "d" ], [ [ "double", "nested" ] ] ]; + * input.reduce(unnestR, []) // [ "a", "b", "c", "d", "double, "nested" ] + * ``` + */ + var flattenR = function (memo, elem) { + return isArray(elem) ? memo.concat(elem.reduce(flattenR, [])) : pushR(memo, elem); + }; + /** + * Reduce function that pushes an object to an array, then returns the array. + * Mostly just for [[flattenR]] and [[uniqR]] + */ + function pushR(arr, obj) { + arr.push(obj); + return arr; + } + /** Reduce function that filters out duplicates */ + var uniqR = function (acc, token) { + return inArray(acc, token) ? acc : pushR(acc, token); + }; + /** + * Return a new array with a single level of arrays unnested. + * + * @example + * ``` + * + * let input = [ [ "a", "b" ], [ "c", "d" ], [ [ "double", "nested" ] ] ]; + * unnest(input) // [ "a", "b", "c", "d", [ "double, "nested" ] ] + * ``` + */ + var unnest = function (arr) { return arr.reduce(unnestR, []); }; + /** + * Return a completely flattened version of an array. + * + * @example + * ``` + * + * let input = [ [ "a", "b" ], [ "c", "d" ], [ [ "double", "nested" ] ] ]; + * flatten(input) // [ "a", "b", "c", "d", "double, "nested" ] + * ``` + */ + var flatten = function (arr) { return arr.reduce(flattenR, []); }; + /** + * Given a .filter Predicate, builds a .filter Predicate which throws an error if any elements do not pass. + * @example + * ``` + * + * let isNumber = (obj) => typeof(obj) === 'number'; + * let allNumbers = [ 1, 2, 3, 4, 5 ]; + * allNumbers.filter(assertPredicate(isNumber)); //OK + * + * let oneString = [ 1, 2, 3, 4, "5" ]; + * oneString.filter(assertPredicate(isNumber, "Not all numbers")); // throws Error(""Not all numbers""); + * ``` + */ + var assertPredicate = assertFn; + /** + * Given a .map function, builds a .map function which throws an error if any mapped elements do not pass a truthyness test. + * @example + * ``` + * + * var data = { foo: 1, bar: 2 }; + * + * let keys = [ 'foo', 'bar' ] + * let values = keys.map(assertMap(key => data[key], "Key not found")); + * // values is [1, 2] + * + * let keys = [ 'foo', 'bar', 'baz' ] + * let values = keys.map(assertMap(key => data[key], "Key not found")); + * // throws Error("Key not found") + * ``` + */ + var assertMap = assertFn; + function assertFn(predicateOrMap, errMsg) { + if (errMsg === void 0) { errMsg = "assert failure"; } + return function (obj) { + var result = predicateOrMap(obj); + if (!result) { + throw new Error(isFunction(errMsg) ? errMsg(obj) : errMsg); + } + return result; + }; + } + /** + * Like _.pairs: Given an object, returns an array of key/value pairs + * + * @example + * ``` + * + * pairs({ foo: "FOO", bar: "BAR }) // [ [ "foo", "FOO" ], [ "bar": "BAR" ] ] + * ``` + */ + var pairs = function (obj) { + return Object.keys(obj).map(function (key) { return [key, obj[key]]; }); + }; + /** + * Given two or more parallel arrays, returns an array of tuples where + * each tuple is composed of [ a[i], b[i], ... z[i] ] + * + * @example + * ``` + * + * let foo = [ 0, 2, 4, 6 ]; + * let bar = [ 1, 3, 5, 7 ]; + * let baz = [ 10, 30, 50, 70 ]; + * arrayTuples(foo, bar); // [ [0, 1], [2, 3], [4, 5], [6, 7] ] + * arrayTuples(foo, bar, baz); // [ [0, 1, 10], [2, 3, 30], [4, 5, 50], [6, 7, 70] ] + * ``` + */ + function arrayTuples() { + var args = []; + for (var _i = 0; _i < arguments.length; _i++) { + args[_i] = arguments[_i]; + } + if (args.length === 0) + return []; + var maxArrayLen = args.reduce(function (min, arr) { return Math.min(arr.length, min); }, 9007199254740991); // aka 2^53 − 1 aka Number.MAX_SAFE_INTEGER + var i, result = []; + for (i = 0; i < maxArrayLen; i++) { + // This is a hot function + // Unroll when there are 1-4 arguments + switch (args.length) { + case 1: + result.push([args[0][i]]); + break; + case 2: + result.push([args[0][i], args[1][i]]); + break; + case 3: + result.push([args[0][i], args[1][i], args[2][i]]); + break; + case 4: + result.push([args[0][i], args[1][i], args[2][i], args[3][i]]); + break; + default: + result.push(args.map(function (array) { return array[i]; })); + break; } } - return extend({}, inherited, newParams); + return result; } - /** - * Normalizes a set of values to string or `null`, filtering them by a list of keys. + * Reduce function which builds an object from an array of [key, value] pairs. * - * @param {Array} keys The list of keys to normalize/return. - * @param {Object} values An object hash of values to normalize. - * @return {Object} Returns an object hash of normalized string values. + * Each iteration sets the key/val pair on the memo object, then returns the memo for the next iteration. + * + * Each keyValueTuple should be an array with values [ key: string, value: any ] + * + * @example + * ``` + * + * var pairs = [ ["fookey", "fooval"], ["barkey", "barval"] ] + * + * var pairsToObj = pairs.reduce((memo, pair) => applyPairs(memo, pair), {}) + * // pairsToObj == { fookey: "fooval", barkey: "barval" } + * + * // Or, more simply: + * var pairsToObj = pairs.reduce(applyPairs, {}) + * // pairsToObj == { fookey: "fooval", barkey: "barval" } + * ``` */ - function normalize(keys, values) { - var normalized = {}; - - forEach(keys, function (name) { - var value = values[name]; - normalized[name] = (value != null) ? String(value) : null; - }); - return normalized; + function applyPairs(memo, keyValTuple) { + var key, value; + if (isArray(keyValTuple)) + key = keyValTuple[0], value = keyValTuple[1]; + if (!isString(key)) + throw new Error("invalid parameters to applyPairs"); + memo[key] = value; + return memo; + } + /** Get the last element of an array */ + function tail(arr) { + return arr.length && arr[arr.length - 1] || undefined; } - /** - * Performs a non-strict comparison of the subset of two objects, defined by a list of keys. - * - * @param {Object} a The first object. - * @param {Object} b The second object. - * @param {Array} keys The list of keys within each object to compare. If the list is empty or not specified, - * it defaults to the list of keys in `a`. - * @return {Boolean} Returns `true` if the keys match, otherwise `false`. + * shallow copy from src to dest */ - function equalForKeys(a, b, keys) { - if (!keys) { - keys = []; - for (var n in a) keys.push(n); // Used instead of Object.keys() for IE8 compatibility + function copy(src, dest) { + if (dest) + Object.keys(dest).forEach(function (key) { return delete dest[key]; }); + if (!dest) + dest = {}; + return extend(dest, src); + } + /** Naive forEach implementation works with Objects or Arrays */ + function _forEach(obj, cb, _this) { + if (isArray(obj)) + return obj.forEach(cb, _this); + Object.keys(obj).forEach(function (key) { return cb(obj[key], key); }); + } + function _extend(toObj) { + for (var i = 1; i < arguments.length; i++) { + var obj = arguments[i]; + if (!obj) + continue; + var keys = Object.keys(obj); + for (var j = 0; j < keys.length; j++) { + toObj[keys[j]] = obj[keys[j]]; + } } - - for (var i=0; i ")); - } - visited[key] = VISIT_IN_PROGRESS; - - if (isString(value)) { - plan.push(key, [ function() { return $injector.get(value); }], NO_DEPENDENCIES); - } else { - var params = $injector.annotate(value); - forEach(params, function (param) { - if (param !== key && invocables.hasOwnProperty(param)) visit(invocables[param], param); - }); - plan.push(key, value, params); - } - - cycle.pop(); - visited[key] = VISIT_DONE; + * @module common + */ /** for typedoc */ + var Queue = /** @class */ (function () { + function Queue(_items, _limit) { + if (_items === void 0) { _items = []; } + if (_limit === void 0) { _limit = null; } + this._items = _items; + this._limit = _limit; } - forEach(invocables, visit); - invocables = cycle = visited = null; // plan is all that's required - - function isResolve(value) { - return isObject(value) && value.then && value.$$promises; - } - - return function (locals, parent, self) { - if (isResolve(locals) && self === undefined) { - self = parent; parent = locals; locals = null; - } - if (!locals) locals = NO_LOCALS; - else if (!isObject(locals)) { - throw new Error("'locals' must be an object"); - } - if (!parent) parent = NO_PARENT; - else if (!isResolve(parent)) { - throw new Error("'parent' must be a promise returned by $resolve.resolve()"); - } - - // To complete the overall resolution, we have to wait for the parent - // promise and for the promise for each invokable in our plan. - var resolution = $q.defer(), - result = resolution.promise, - promises = result.$$promises = {}, - values = extend({}, locals), - wait = 1 + plan.length/3, - merged = false; - - function done() { - // Merge parent values we haven't got yet and publish our own $$values - if (!--wait) { - if (!merged) merge(values, parent.$$values); - result.$$values = values; - result.$$promises = true; // keep for isResolve() - resolution.resolve(values); - } - } - - function fail(reason) { - result.$$failure = reason; - resolution.reject(reason); - } - - // Short-circuit if parent has already failed - if (isDefined(parent.$$failure)) { - fail(parent.$$failure); - return result; - } - - // Merge parent values if the parent has already resolved, or merge - // parent promises and wait if the parent resolve is still in progress. - if (parent.$$values) { - merged = merge(values, parent.$$values); - done(); - } else { - extend(promises, parent.$$promises); - parent.then(done, fail); - } - - // Process each invocable in the plan, but ignore any where a local of the same name exists. - for (var i=0, ii=plan.length; i this._limit) + items.shift(); + return item; }; - }; - - /** - * Resolves a set of invocables. An invocable is a function to be invoked via `$injector.invoke()`, - * and can have an arbitrary number of dependencies. An invocable can either return a value directly, - * or a `$q` promise. If a promise is returned it will be resolved and the resulting value will be - * used instead. Dependencies of invocables are resolved (in this order of precedence) - * - * - from the specified `locals` - * - from another invocable that is part of this `$resolve` call - * - from an invocable that is inherited from a `parent` call to `$resolve` (or recursively - * from any ancestor `$resolve` of that parent). - * - * The return value of `$resolve` is a promise for an object that contains (in this order of precedence) - * - * - any `locals` (if specified) - * - the resolved return values of all injectables - * - any values inherited from a `parent` call to `$resolve` (if specified) - * - * The promise will resolve after the `parent` promise (if any) and all promises returned by injectables - * have been resolved. If any invocable (or `$injector.invoke`) throws an exception, or if a promise - * returned by an invocable is rejected, the `$resolve` promise is immediately rejected with the same error. - * A rejection of a `parent` promise (if specified) will likewise be propagated immediately. Once the - * `$resolve` promise has been rejected, no further invocables will be called. - * - * Cyclic dependencies between invocables are not permitted and will caues `$resolve` to throw an - * error. As a special case, an injectable can depend on a parameter with the same name as the injectable, - * which will be fulfilled from the `parent` injectable of the same name. This allows inherited values - * to be decorated. Note that in this case any other injectable in the same `$resolve` with the same - * dependency would see the decorated value, not the inherited value. - * - * Note that missing dependencies -- unlike cyclic dependencies -- will cause an (asynchronous) rejection - * of the `$resolve` promise rather than a (synchronous) exception. - * - * Invocables are invoked eagerly as soon as all dependencies are available. This is true even for - * dependencies inherited from a `parent` call to `$resolve`. - * - * As a special case, an invocable can be a string, in which case it is taken to be a service name - * to be passed to `$injector.get()`. This is supported primarily for backwards-compatibility with the - * `resolve` property of `$routeProvider` routes. - * - * @function - * @param {Object.} invocables functions to invoke or `$injector` services to fetch. - * @param {Object.} [locals] values to make available to the injectables - * @param {Promise.} [parent] a promise returned by another call to `$resolve`. - * @param {Object} [self] the `this` for the invoked methods - * @return {Promise.} Promise for an object that contains the resolved return value - * of all invocables, as well as any inherited and local values. - */ - this.resolve = function (invocables, locals, parent, self) { - return this.study(invocables)(locals, parent, self); - }; - } - - angular.module('ui.router.util').service('$resolve', $Resolve); - + Queue.prototype.dequeue = function () { + if (this.size()) + return this._items.splice(0, 1)[0]; + }; + Queue.prototype.clear = function () { + var current = this._items; + this._items = []; + return current; + }; + Queue.prototype.size = function () { + return this._items.length; + }; + Queue.prototype.remove = function (item) { + var idx = this._items.indexOf(item); + return idx > -1 && this._items.splice(idx, 1)[0]; + }; + Queue.prototype.peekTail = function () { + return this._items[this._items.length - 1]; + }; + Queue.prototype.peekHead = function () { + if (this.size()) + return this._items[0]; + }; + return Queue; + }()); /** - * Service. Manages loading of templates. - * @constructor - * @name $templateFactory - * @requires $http - * @requires $templateCache - * @requires $injector - */ - $TemplateFactory.$inject = ['$http', '$templateCache', '$injector']; - function $TemplateFactory( $http, $templateCache, $injector) { + * @coreapi + * @module transition + */ /** for typedoc */ + "use strict"; - /** - * Creates a template from a configuration object. - * @function - * @name $templateFactory#fromConfig - * @methodOf $templateFactory - * @param {Object} config Configuration object for which to load a template. The following - * properties are search in the specified order, and the first one that is defined is - * used to create the template: - * @param {string|Function} config.template html string template or function to load via - * {@link $templateFactory#fromString fromString}. - * @param {string|Function} config.templateUrl url to load or a function returning the url - * to load via {@link $templateFactory#fromUrl fromUrl}. - * @param {Function} config.templateProvider function to invoke via - * {@link $templateFactory#fromProvider fromProvider}. - * @param {Object} params Parameters to pass to the template function. - * @param {Object} [locals] Locals to pass to `invoke` if the template is loaded via a - * `templateProvider`. Defaults to `{ params: params }`. - * @return {string|Promise.} The template html as a string, or a promise for that string, - * or `null` if no template is configured. - */ - this.fromConfig = function (config, params, locals) { - return ( - isDefined(config.template) ? this.fromString(config.template, params) : - isDefined(config.templateUrl) ? this.fromUrl(config.templateUrl, params) : - isDefined(config.templateProvider) ? this.fromProvider(config.templateProvider, params, locals) : - null - ); + (function (RejectType) { + RejectType[RejectType["SUPERSEDED"] = 2] = "SUPERSEDED"; + RejectType[RejectType["ABORTED"] = 3] = "ABORTED"; + RejectType[RejectType["INVALID"] = 4] = "INVALID"; + RejectType[RejectType["IGNORED"] = 5] = "IGNORED"; + RejectType[RejectType["ERROR"] = 6] = "ERROR"; + })(exports.RejectType || (exports.RejectType = {})); + /** @hidden */ var id = 0; + var Rejection = /** @class */ (function () { + function Rejection(type, message, detail) { + this.$id = id++; + this.type = type; + this.message = message; + this.detail = detail; + } + Rejection.prototype.toString = function () { + var detailString = function (d) { + return d && d.toString !== Object.prototype.toString ? d.toString() : stringify(d); + }; + var detail = detailString(this.detail); + var _a = this, $id = _a.$id, type = _a.type, message = _a.message; + return "Transition Rejection($id: " + $id + " type: " + type + ", message: " + message + ", detail: " + detail + ")"; }; - - /** - * Creates a template from a string or a function returning a string. - * @function - * @name $templateFactory#fromString - * @methodOf $templateFactory - * @param {string|Function} template html template as a string or function that returns an html - * template as a string. - * @param {Object} params Parameters to pass to the template function. - * @return {string|Promise.} The template html as a string, or a promise for that string. - */ - this.fromString = function (template, params) { - return isFunction(template) ? template(params) : template; + Rejection.prototype.toPromise = function () { + return extend(silentRejection(this), { _transitionRejection: this }); }; - - /** - * Loads a template from the a URL via `$http` and `$templateCache`. - * @function - * @name $templateFactory#fromUrl - * @methodOf $templateFactory - * @param {string|Function} url url of the template to load, or a function that returns a url. - * @param {Object} params Parameters to pass to the url function. - * @return {string|Promise.} The template html as a string, or a promise for that string. - */ - this.fromUrl = function (url, params) { - if (isFunction(url)) url = url(params); - if (url == null) return null; - else return $http - .get(url, { cache: $templateCache }) - .then(function(response) { return response.data; }); + /** Returns true if the obj is a rejected promise created from the `asPromise` factory */ + Rejection.isRejectionPromise = function (obj) { + return obj && (typeof obj.then === 'function') && is(Rejection)(obj._transitionRejection); }; - - /** - * Creates a template by invoking an injectable provider function. - * @function - * @name $templateFactory#fromUrl - * @methodOf $templateFactory - * @param {Function} provider Function to invoke via `$injector.invoke` - * @param {Object} params Parameters for the template. - * @param {Object} [locals] Locals to pass to `invoke`. Defaults to `{ params: params }`. - * @return {string|Promise.} The template html as a string, or a promise for that string. - */ - this.fromProvider = function (provider, params, locals) { - return $injector.invoke(provider, null, locals || { params: params }); + /** Returns a Rejection due to transition superseded */ + Rejection.superseded = function (detail, options) { + var message = "The transition has been superseded by a different transition"; + var rejection = new Rejection(exports.RejectType.SUPERSEDED, message, detail); + if (options && options.redirected) { + rejection.redirected = true; + } + return rejection; }; - } - - angular.module('ui.router.util').service('$templateFactory', $TemplateFactory); + /** Returns a Rejection due to redirected transition */ + Rejection.redirected = function (detail) { + return Rejection.superseded(detail, { redirected: true }); + }; + /** Returns a Rejection due to invalid transition */ + Rejection.invalid = function (detail) { + var message = "This transition is invalid"; + return new Rejection(exports.RejectType.INVALID, message, detail); + }; + /** Returns a Rejection due to ignored transition */ + Rejection.ignored = function (detail) { + var message = "The transition was ignored"; + return new Rejection(exports.RejectType.IGNORED, message, detail); + }; + /** Returns a Rejection due to aborted transition */ + Rejection.aborted = function (detail) { + var message = "The transition has been aborted"; + return new Rejection(exports.RejectType.ABORTED, message, detail); + }; + /** Returns a Rejection due to aborted transition */ + Rejection.errored = function (detail) { + var message = "The transition errored"; + return new Rejection(exports.RejectType.ERROR, message, detail); + }; + /** + * Returns a Rejection + * + * Normalizes a value as a Rejection. + * If the value is already a Rejection, returns it. + * Otherwise, wraps and returns the value as a Rejection (Rejection type: ERROR). + * + * @returns `detail` if it is already a `Rejection`, else returns an ERROR Rejection. + */ + Rejection.normalize = function (detail) { + return is(Rejection)(detail) ? detail : Rejection.errored(detail); + }; + return Rejection; + }()); /** - * Matches URLs against patterns and extracts named parameters from the path or the search - * part of the URL. A URL pattern consists of a path pattern, optionally followed by '?' and a list - * of search parameters. Multiple search parameter names are separated by '&'. Search parameters - * do not influence whether or not a URL is matched, but their values are passed through into - * the matched parameters returned by {@link UrlMatcher#exec exec}. + * # Transition tracing (debug) * - * Path parameter placeholders can be specified using simple colon/catch-all syntax or curly brace - * syntax, which optionally allows a regular expression for the parameter to be specified: + * Enable transition tracing to print transition information to the console, + * in order to help debug your application. + * Tracing logs detailed information about each Transition to your console. * - * * ':' name - colon placeholder - * * '*' name - catch-all placeholder - * * '{' name '}' - curly placeholder - * * '{' name ':' regexp '}' - curly placeholder with regexp. Should the regexp itself contain - * curly braces, they must be in matched pairs or escaped with a backslash. + * To enable tracing, import the [[Trace]] singleton and enable one or more categories. * - * Parameter names may contain only word characters (latin letters, digits, and underscore) and - * must be unique within the pattern (across both path and search parameters). For colon - * placeholders or curly placeholders without an explicit regexp, a path parameter matches any - * number of characters other than '/'. For catch-all placeholders the path parameter matches - * any number of characters. + * ### ES6 + * ```js + * import {trace} from "ui-router-ng2"; // or "angular-ui-router" + * trace.enable(1, 5); // TRANSITION and VIEWCONFIG + * ``` * - * ### Examples + * ### CJS + * ```js + * let trace = require("angular-ui-router").trace; // or "ui-router-ng2" + * trace.enable("TRANSITION", "VIEWCONFIG"); + * ``` * - * * '/hello/' - Matches only if the path is exactly '/hello/'. There is no special treatment for - * trailing slashes, and patterns have to match the entire path, not just a prefix. - * * '/user/:id' - Matches '/user/bob' or '/user/1234!!!' or even '/user/' but not '/user' or - * '/user/bob/details'. The second path segment will be captured as the parameter 'id'. - * * '/user/{id}' - Same as the previous example, but using curly brace syntax. - * * '/user/{id:[^/]*}' - Same as the previous example. - * * '/user/{id:[0-9a-fA-F]{1,8}}' - Similar to the previous example, but only matches if the id - * parameter consists of 1 to 8 hex digits. - * * '/files/{path:.*}' - Matches any URL starting with '/files/' and captures the rest of the - * path into the parameter 'path'. - * * '/files/*path' - ditto. + * ### Globals + * ```js + * let trace = window["angular-ui-router"].trace; // or "ui-router-ng2" + * trace.enable(); // Trace everything (very verbose) + * ``` * - * @constructor - * @param {string} pattern the pattern to compile into a matcher. + * ### Angular 1: + * ```js + * app.run($trace => $trace.enable()); + * ``` * - * @property {string} prefix A static prefix of this pattern. The matcher guarantees that any - * URL matching this matcher (i.e. any string for which {@link UrlMatcher#exec exec()} returns - * non-null) will start with this prefix. + * @coreapi + * @module trace + */ /** for typedoc */ + /** @hidden */ + function uiViewString(uiview) { + if (!uiview) + return 'ui-view (defunct)'; + var state = uiview.creationContext ? uiview.creationContext.name || '(root)' : '(none)'; + return "[ui-view#" + uiview.id + " " + uiview.$type + ":" + uiview.fqn + " (" + uiview.name + "@" + state + ")]"; + } + /** @hidden */ + var viewConfigString = function (viewConfig) { + var view = viewConfig.viewDecl; + var state = view.$context.name || '(root)'; + return "[View#" + viewConfig.$id + " from '" + state + "' state]: target ui-view: '" + view.$uiViewName + "@" + view.$uiViewContextAnchor + "'"; + }; + /** @hidden */ + function normalizedCat(input) { + return isNumber(input) ? exports.Category[input] : exports.Category[exports.Category[input]]; + } + /** @hidden */ + var consoleLog = Function.prototype.bind.call(console.log, console); + /** @hidden */ + var consoletable = isFunction(console.table) ? console.table.bind(console) : consoleLog.bind(console); + /** + * Trace categories Enum + * + * Enable or disable a category using [[Trace.enable]] or [[Trace.disable]] + * + * `trace.enable(Category.TRANSITION)` + * + * These can also be provided using a matching string, or position ordinal + * + * `trace.enable("TRANSITION")` + * + * `trace.enable(1)` */ - function UrlMatcher(pattern) { - // Find all placeholders and create a compiled pattern, using either classic or curly syntax: - // '*' name - // ':' name - // '{' name '}' - // '{' name ':' regexp '}' - // The regular expression is somewhat complicated due to the need to allow curly braces - // inside the regular expression. The placeholder regexp breaks down as follows: - // ([:*])(\w+) classic placeholder ($1 / $2) - // \{(\w+)(?:\:( ... ))?\} curly brace placeholder ($3) with optional regexp ... ($4) - // (?: ... | ... | ... )+ the regexp consists of any number of atoms, an atom being either - // [^{}\\]+ - anything other than curly braces or backslash - // \\. - a backslash escape - // \{(?:[^{}\\]+|\\.)*\} - a matched set of curly braces containing other atoms - var placeholder = /([:*])(\w+)|\{(\w+)(?:\:((?:[^{}\\]+|\\.|\{(?:[^{}\\]+|\\.)*\})+))?\}/g, - names = {}, compiled = '^', last = 0, m, - segments = this.segments = [], - params = this.params = []; - - function addParameter(id) { - if (!/^\w+(-+\w+)*$/.test(id)) throw new Error("Invalid parameter name '" + id + "' in pattern '" + pattern + "'"); - if (names[id]) throw new Error("Duplicate parameter name '" + id + "' in pattern '" + pattern + "'"); - names[id] = true; - params.push(id); + (function (Category) { + Category[Category["RESOLVE"] = 0] = "RESOLVE"; + Category[Category["TRANSITION"] = 1] = "TRANSITION"; + Category[Category["HOOK"] = 2] = "HOOK"; + Category[Category["UIVIEW"] = 3] = "UIVIEW"; + Category[Category["VIEWCONFIG"] = 4] = "VIEWCONFIG"; + })(exports.Category || (exports.Category = {})); + /** @hidden */ var _tid = parse("$id"); + /** @hidden */ var _rid = parse("router.$id"); + /** @hidden */ var transLbl = function (trans) { return "Transition #" + _tid(trans) + "-" + _rid(trans); }; + /** + * Prints UI-Router Transition trace information to the console. + */ + var Trace = /** @class */ (function () { + /** @hidden */ + function Trace() { + /** @hidden */ + this._enabled = {}; + this.approximateDigests = 0; } + /** @hidden */ + Trace.prototype._set = function (enabled, categories) { + var _this = this; + if (!categories.length) { + categories = Object.keys(exports.Category) + .map(function (k) { return parseInt(k, 10); }) + .filter(function (k) { return !isNaN(k); }) + .map(function (key) { return exports.Category[key]; }); + } + categories.map(normalizedCat).forEach(function (category) { return _this._enabled[category] = enabled; }); + }; + Trace.prototype.enable = function () { + var categories = []; + for (var _i = 0; _i < arguments.length; _i++) { + categories[_i] = arguments[_i]; + } + this._set(true, categories); + }; + Trace.prototype.disable = function () { + var categories = []; + for (var _i = 0; _i < arguments.length; _i++) { + categories[_i] = arguments[_i]; + } + this._set(false, categories); + }; + /** + * Retrieves the enabled stateus of a [[Category]] + * + * ```js + * trace.enabled("VIEWCONFIG"); // true or false + * ``` + * + * @returns boolean true if the category is enabled + */ + Trace.prototype.enabled = function (category) { + return !!this._enabled[normalizedCat(category)]; + }; + /** @internalapi called by ui-router code */ + Trace.prototype.traceTransitionStart = function (trans) { + if (!this.enabled(exports.Category.TRANSITION)) + return; + console.log(transLbl(trans) + ": Started -> " + stringify(trans)); + }; + /** @internalapi called by ui-router code */ + Trace.prototype.traceTransitionIgnored = function (trans) { + if (!this.enabled(exports.Category.TRANSITION)) + return; + console.log(transLbl(trans) + ": Ignored <> " + stringify(trans)); + }; + /** @internalapi called by ui-router code */ + Trace.prototype.traceHookInvocation = function (step, trans, options) { + if (!this.enabled(exports.Category.HOOK)) + return; + var event = parse("traceData.hookType")(options) || "internal", context = parse("traceData.context.state.name")(options) || parse("traceData.context")(options) || "unknown", name = functionToString(step.registeredHook.callback); + console.log(transLbl(trans) + ": Hook -> " + event + " context: " + context + ", " + maxLength(200, name)); + }; + /** @internalapi called by ui-router code */ + Trace.prototype.traceHookResult = function (hookResult, trans, transitionOptions) { + if (!this.enabled(exports.Category.HOOK)) + return; + console.log(transLbl(trans) + ": <- Hook returned: " + maxLength(200, stringify(hookResult))); + }; + /** @internalapi called by ui-router code */ + Trace.prototype.traceResolvePath = function (path, when, trans) { + if (!this.enabled(exports.Category.RESOLVE)) + return; + console.log(transLbl(trans) + ": Resolving " + path + " (" + when + ")"); + }; + /** @internalapi called by ui-router code */ + Trace.prototype.traceResolvableResolved = function (resolvable, trans) { + if (!this.enabled(exports.Category.RESOLVE)) + return; + console.log(transLbl(trans) + ": <- Resolved " + resolvable + " to: " + maxLength(200, stringify(resolvable.data))); + }; + /** @internalapi called by ui-router code */ + Trace.prototype.traceError = function (reason, trans) { + if (!this.enabled(exports.Category.TRANSITION)) + return; + console.log(transLbl(trans) + ": <- Rejected " + stringify(trans) + ", reason: " + reason); + }; + /** @internalapi called by ui-router code */ + Trace.prototype.traceSuccess = function (finalState, trans) { + if (!this.enabled(exports.Category.TRANSITION)) + return; + console.log(transLbl(trans) + ": <- Success " + stringify(trans) + ", final state: " + finalState.name); + }; + /** @internalapi called by ui-router code */ + Trace.prototype.traceUIViewEvent = function (event, viewData, extra) { + if (extra === void 0) { extra = ""; } + if (!this.enabled(exports.Category.UIVIEW)) + return; + console.log("ui-view: " + padString(30, event) + " " + uiViewString(viewData) + extra); + }; + /** @internalapi called by ui-router code */ + Trace.prototype.traceUIViewConfigUpdated = function (viewData, context) { + if (!this.enabled(exports.Category.UIVIEW)) + return; + this.traceUIViewEvent("Updating", viewData, " with ViewConfig from context='" + context + "'"); + }; + /** @internalapi called by ui-router code */ + Trace.prototype.traceUIViewFill = function (viewData, html) { + if (!this.enabled(exports.Category.UIVIEW)) + return; + this.traceUIViewEvent("Fill", viewData, " with: " + maxLength(200, html)); + }; + /** @internalapi called by ui-router code */ + Trace.prototype.traceViewSync = function (pairs) { + if (!this.enabled(exports.Category.VIEWCONFIG)) + return; + var mapping = pairs.map(function (_a) { + var uiViewData = _a[0], config = _a[1]; + var uiView = uiViewData.$type + ":" + uiViewData.fqn; + var view = config && config.viewDecl.$context.name + ": " + config.viewDecl.$name + " (" + config.viewDecl.$type + ")"; + return { 'ui-view fqn': uiView, 'state: view name': view }; + }).sort(function (a, b) { return a['ui-view fqn'].localeCompare(b['ui-view fqn']); }); + consoletable(mapping); + }; + /** @internalapi called by ui-router code */ + Trace.prototype.traceViewServiceEvent = function (event, viewConfig) { + if (!this.enabled(exports.Category.VIEWCONFIG)) + return; + console.log("VIEWCONFIG: " + event + " " + viewConfigString(viewConfig)); + }; + /** @internalapi called by ui-router code */ + Trace.prototype.traceViewServiceUIViewEvent = function (event, viewData) { + if (!this.enabled(exports.Category.VIEWCONFIG)) + return; + console.log("VIEWCONFIG: " + event + " " + uiViewString(viewData)); + }; + return Trace; + }()); + /** + * The [[Trace]] singleton + * + * #### Example: + * ```js + * import {trace} from "angular-ui-router"; + * trace.enable(1, 5); + * ``` + */ + var trace = new Trace(); - function quoteRegExp(string) { - return string.replace(/[\\\[\]\^$*+?.()|{}]/g, "\\$&"); + (function (TransitionHookPhase) { + TransitionHookPhase[TransitionHookPhase["CREATE"] = 0] = "CREATE"; + TransitionHookPhase[TransitionHookPhase["BEFORE"] = 1] = "BEFORE"; + TransitionHookPhase[TransitionHookPhase["RUN"] = 2] = "RUN"; + TransitionHookPhase[TransitionHookPhase["SUCCESS"] = 3] = "SUCCESS"; + TransitionHookPhase[TransitionHookPhase["ERROR"] = 4] = "ERROR"; + })(exports.TransitionHookPhase || (exports.TransitionHookPhase = {})); + + (function (TransitionHookScope) { + TransitionHookScope[TransitionHookScope["TRANSITION"] = 0] = "TRANSITION"; + TransitionHookScope[TransitionHookScope["STATE"] = 1] = "STATE"; + })(exports.TransitionHookScope || (exports.TransitionHookScope = {})); + + /** + * @coreapi + * @module state + */ /** for typedoc */ + /** + * Encapsulate the target (destination) state/params/options of a [[Transition]]. + * + * This class is frequently used to redirect a transition to a new destination. + * + * See: + * + * - [[HookResult]] + * - [[TransitionHookFn]] + * - [[TransitionService.onStart]] + * + * To create a `TargetState`, use [[StateService.target]]. + * + * --- + * + * This class wraps: + * + * 1) an identifier for a state + * 2) a set of parameters + * 3) and transition options + * 4) the registered state object (the [[StateDeclaration]]) + * + * Many UI-Router APIs such as [[StateService.go]] take a [[StateOrName]] argument which can + * either be a *state object* (a [[StateDeclaration]] or [[StateObject]]) or a *state name* (a string). + * The `TargetState` class normalizes those options. + * + * A `TargetState` may be valid (the state being targeted exists in the registry) + * or invalid (the state being targeted is not registered). + */ + var TargetState = /** @class */ (function () { + /** + * The TargetState constructor + * + * Note: Do not construct a `TargetState` manually. + * To create a `TargetState`, use the [[StateService.target]] factory method. + * + * @param _stateRegistry The StateRegistry to use to look up the _definition + * @param _identifier An identifier for a state. + * Either a fully-qualified state name, or the object used to define the state. + * @param _params Parameters for the target state + * @param _options Transition options. + * + * @internalapi + */ + function TargetState(_stateRegistry, _identifier, _params, _options) { + this._stateRegistry = _stateRegistry; + this._identifier = _identifier; + this._identifier = _identifier; + this._params = extend({}, _params || {}); + this._options = extend({}, _options || {}); + this._definition = _stateRegistry.matcher.find(_identifier, this._options.relative); } + /** The name of the state this object targets */ + TargetState.prototype.name = function () { + return this._definition && this._definition.name || this._identifier; + }; + /** The identifier used when creating this TargetState */ + TargetState.prototype.identifier = function () { + return this._identifier; + }; + /** The target parameter values */ + TargetState.prototype.params = function () { + return this._params; + }; + /** The internal state object (if it was found) */ + TargetState.prototype.$state = function () { + return this._definition; + }; + /** The internal state declaration (if it was found) */ + TargetState.prototype.state = function () { + return this._definition && this._definition.self; + }; + /** The target options */ + TargetState.prototype.options = function () { + return this._options; + }; + /** True if the target state was found */ + TargetState.prototype.exists = function () { + return !!(this._definition && this._definition.self); + }; + /** True if the object is valid */ + TargetState.prototype.valid = function () { + return !this.error(); + }; + /** If the object is invalid, returns the reason why */ + TargetState.prototype.error = function () { + var base = this.options().relative; + if (!this._definition && !!base) { + var stateName = base.name ? base.name : base; + return "Could not resolve '" + this.name() + "' from state '" + stateName + "'"; + } + if (!this._definition) + return "No such state '" + this.name() + "'"; + if (!this._definition.self) + return "State '" + this.name() + "' has an invalid definition"; + }; + TargetState.prototype.toString = function () { + return "'" + this.name() + "'" + stringify(this.params()); + }; + /** + * Returns a copy of this TargetState which targets a different state. + * The new TargetState has the same parameter values and transition options. + * + * @param state The new state that should be targeted + */ + TargetState.prototype.withState = function (state) { + return new TargetState(this._stateRegistry, state, this._params, this._options); + }; + /** + * Returns a copy of this TargetState, using the specified parameter values. + * + * @param params the new parameter values to use + * @param replace When false (default) the new parameter values will be merged with the current values. + * When true the parameter values will be used instead of the current values. + */ + TargetState.prototype.withParams = function (params, replace) { + if (replace === void 0) { replace = false; } + var newParams = replace ? params : extend({}, this._params, params); + return new TargetState(this._stateRegistry, this._identifier, newParams, this._options); + }; + /** + * Returns a copy of this TargetState, using the specified Transition Options. + * + * @param options the new options to use + * @param replace When false (default) the new options will be merged with the current options. + * When true the options will be used instead of the current options. + */ + TargetState.prototype.withOptions = function (options, replace) { + if (replace === void 0) { replace = false; } + var newOpts = replace ? options : extend({}, this._options, options); + return new TargetState(this._stateRegistry, this._identifier, this._params, newOpts); + }; + /** Returns true if the object has a state property that might be a state or state name */ + TargetState.isDef = function (obj) { + return obj && obj.state && (isString(obj.state) || isString(obj.state.name)); + }; + return TargetState; + }()); - this.source = pattern; - - // Split into static segments separated by path parameter placeholders. - // The number of segments is always 1 more than the number of parameters. - var id, regexp, segment; - while ((m = placeholder.exec(pattern))) { - id = m[2] || m[3]; // IE[78] returns '' for unmatched groups instead of null - regexp = m[4] || (m[1] == '*' ? '.*' : '[^/]*'); - segment = pattern.substring(last, m.index); - if (segment.indexOf('?') >= 0) break; // we're into the search part - compiled += quoteRegExp(segment) + '(' + regexp + ')'; - addParameter(id); - segments.push(segment); - last = placeholder.lastIndex; + /** + * @coreapi + * @module transition + */ + /** for typedoc */ + var defaultOptions = { + current: noop$1, + transition: null, + traceData: {}, + bind: null, + }; + /** @hidden */ + var TransitionHook = /** @class */ (function () { + function TransitionHook(transition, stateContext, registeredHook, options) { + var _this = this; + this.transition = transition; + this.stateContext = stateContext; + this.registeredHook = registeredHook; + this.options = options; + this.isSuperseded = function () { + return _this.type.hookPhase === exports.TransitionHookPhase.RUN && !_this.options.transition.isActive(); + }; + this.options = defaults(options, defaultOptions); + this.type = registeredHook.eventType; } - segment = pattern.substring(last); + TransitionHook.prototype.logError = function (err) { + this.transition.router.stateService.defaultErrorHandler()(err); + }; + TransitionHook.prototype.invokeHook = function () { + var _this = this; + var hook = this.registeredHook; + if (hook._deregistered) + return; + var notCurrent = this.getNotCurrentRejection(); + if (notCurrent) + return notCurrent; + var options = this.options; + trace.traceHookInvocation(this, this.transition, options); + var invokeCallback = function () { + return hook.callback.call(options.bind, _this.transition, _this.stateContext); + }; + var normalizeErr = function (err) { + return Rejection.normalize(err).toPromise(); + }; + var handleError = function (err) { + return hook.eventType.getErrorHandler(_this)(err); + }; + var handleResult = function (result) { + return hook.eventType.getResultHandler(_this)(result); + }; + try { + var result = invokeCallback(); + if (!this.type.synchronous && isPromise(result)) { + return result.catch(normalizeErr) + .then(handleResult, handleError); + } + else { + return handleResult(result); + } + } + catch (err) { + // If callback throws (synchronously) + return handleError(Rejection.normalize(err)); + } + finally { + if (hook.invokeLimit && ++hook.invokeCount >= hook.invokeLimit) { + hook.deregister(); + } + } + }; + /** + * This method handles the return value of a Transition Hook. + * + * A hook can return false (cancel), a TargetState (redirect), + * or a promise (which may later resolve to false or a redirect) + * + * This also handles "transition superseded" -- when a new transition + * was started while the hook was still running + */ + TransitionHook.prototype.handleHookResult = function (result) { + var _this = this; + var notCurrent = this.getNotCurrentRejection(); + if (notCurrent) + return notCurrent; + // Hook returned a promise + if (isPromise(result)) { + // Wait for the promise, then reprocess with the resulting value + return result.then(function (val$$1) { return _this.handleHookResult(val$$1); }); + } + trace.traceHookResult(result, this.transition, this.options); + // Hook returned false + if (result === false) { + // Abort this Transition + return Rejection.aborted("Hook aborted transition").toPromise(); + } + var isTargetState = is(TargetState); + // hook returned a TargetState + if (isTargetState(result)) { + // Halt the current Transition and redirect (a new Transition) to the TargetState. + return Rejection.redirected(result).toPromise(); + } + }; + /** + * Return a Rejection promise if the transition is no longer current due + * to a stopped router (disposed), or a new transition has started and superseded this one. + */ + TransitionHook.prototype.getNotCurrentRejection = function () { + var router = this.transition.router; + // The router is stopped + if (router._disposed) { + return Rejection.aborted("UIRouter instance #" + router.$id + " has been stopped (disposed)").toPromise(); + } + if (this.transition._aborted) { + return Rejection.aborted().toPromise(); + } + // This transition is no longer current. + // Another transition started while this hook was still running. + if (this.isSuperseded()) { + // Abort this transition + return Rejection.superseded(this.options.current()).toPromise(); + } + }; + TransitionHook.prototype.toString = function () { + var _a = this, options = _a.options, registeredHook = _a.registeredHook; + var event = parse("traceData.hookType")(options) || "internal", context = parse("traceData.context.state.name")(options) || parse("traceData.context")(options) || "unknown", name = fnToString(registeredHook.callback); + return event + " context: " + context + ", " + maxLength(200, name); + }; + /** + * Chains together an array of TransitionHooks. + * + * Given a list of [[TransitionHook]] objects, chains them together. + * Each hook is invoked after the previous one completes. + * + * #### Example: + * ```js + * var hooks: TransitionHook[] = getHooks(); + * let promise: Promise = TransitionHook.chain(hooks); + * + * promise.then(handleSuccess, handleError); + * ``` + * + * @param hooks the list of hooks to chain together + * @param waitFor if provided, the chain is `.then()`'ed off this promise + * @returns a `Promise` for sequentially invoking the hooks (in order) + */ + TransitionHook.chain = function (hooks, waitFor) { + // Chain the next hook off the previous + var createHookChainR = function (prev, nextHook) { + return prev.then(function () { return nextHook.invokeHook(); }); + }; + return hooks.reduce(createHookChainR, waitFor || services.$q.when()); + }; + /** + * Invokes all the provided TransitionHooks, in order. + * Each hook's return value is checked. + * If any hook returns a promise, then the rest of the hooks are chained off that promise, and the promise is returned. + * If no hook returns a promise, then all hooks are processed synchronously. + * + * @param hooks the list of TransitionHooks to invoke + * @param doneCallback a callback that is invoked after all the hooks have successfully completed + * + * @returns a promise for the async result, or the result of the callback + */ + TransitionHook.invokeHooks = function (hooks, doneCallback) { + for (var idx = 0; idx < hooks.length; idx++) { + var hookResult = hooks[idx].invokeHook(); + if (isPromise(hookResult)) { + var remainingHooks = hooks.slice(idx + 1); + return TransitionHook.chain(remainingHooks, hookResult) + .then(doneCallback); + } + } + return doneCallback(); + }; + /** + * Run all TransitionHooks, ignoring their return value. + */ + TransitionHook.runAllHooks = function (hooks) { + hooks.forEach(function (hook) { return hook.invokeHook(); }); + }; + /** + * These GetResultHandler(s) are used by [[invokeHook]] below + * Each HookType chooses a GetResultHandler (See: [[TransitionService._defineCoreEvents]]) + */ + TransitionHook.HANDLE_RESULT = function (hook) { return function (result) { + return hook.handleHookResult(result); + }; }; + /** + * If the result is a promise rejection, log it. + * Otherwise, ignore the result. + */ + TransitionHook.LOG_REJECTED_RESULT = function (hook) { return function (result) { + isPromise(result) && result.catch(function (err) { + return hook.logError(Rejection.normalize(err)); + }); + return undefined; + }; }; + /** + * These GetErrorHandler(s) are used by [[invokeHook]] below + * Each HookType chooses a GetErrorHandler (See: [[TransitionService._defineCoreEvents]]) + */ + TransitionHook.LOG_ERROR = function (hook) { return function (error) { + return hook.logError(error); + }; }; + TransitionHook.REJECT_ERROR = function (hook) { return function (error) { + return silentRejection(error); + }; }; + TransitionHook.THROW_ERROR = function (hook) { return function (error) { + throw error; + }; }; + return TransitionHook; + }()); - // Find any search parameter names and remove them from the last segment - var i = segment.indexOf('?'); - if (i >= 0) { - var search = this.sourceSearch = segment.substring(i); - segment = segment.substring(0, i); - this.sourcePath = pattern.substring(0, last+i); - - // Allow parameters to be separated by '?' as well as '&' to make concat() easier - forEach(search.substring(1).split(/[&?]/), addParameter); - } else { - this.sourcePath = pattern; - this.sourceSearch = ''; + /** + * @coreapi + * @module transition + */ /** for typedoc */ + /** + * Determines if the given state matches the matchCriteria + * + * @hidden + * + * @param state a State Object to test against + * @param criterion + * - If a string, matchState uses the string as a glob-matcher against the state name + * - If an array (of strings), matchState uses each string in the array as a glob-matchers against the state name + * and returns a positive match if any of the globs match. + * - If a function, matchState calls the function with the state and returns true if the function's result is truthy. + * @returns {boolean} + */ + function matchState(state, criterion) { + var toMatch = isString(criterion) ? [criterion] : criterion; + function matchGlobs(_state) { + var globStrings = toMatch; + for (var i = 0; i < globStrings.length; i++) { + var glob = new Glob(globStrings[i]); + if ((glob && glob.matches(_state.name)) || (!glob && globStrings[i] === _state.name)) { + return true; + } + } + return false; } - - compiled += quoteRegExp(segment) + '$'; - segments.push(segment); - this.regexp = new RegExp(compiled); - this.prefix = segments[0]; + var matchFn = (isFunction(toMatch) ? toMatch : matchGlobs); + return !!matchFn(state); + } + /** + * @internalapi + * The registration data for a registered transition hook + */ + var RegisteredHook = /** @class */ (function () { + function RegisteredHook(tranSvc, eventType, callback, matchCriteria, removeHookFromRegistry, options) { + if (options === void 0) { options = {}; } + this.tranSvc = tranSvc; + this.eventType = eventType; + this.callback = callback; + this.matchCriteria = matchCriteria; + this.removeHookFromRegistry = removeHookFromRegistry; + this.invokeCount = 0; + this._deregistered = false; + this.priority = options.priority || 0; + this.bind = options.bind || null; + this.invokeLimit = options.invokeLimit; + } + /** + * Gets the matching [[PathNode]]s + * + * Given an array of [[PathNode]]s, and a [[HookMatchCriterion]], returns an array containing + * the [[PathNode]]s that the criteria matches, or `null` if there were no matching nodes. + * + * Returning `null` is significant to distinguish between the default + * "match-all criterion value" of `true` compared to a `() => true` function, + * when the nodes is an empty array. + * + * This is useful to allow a transition match criteria of `entering: true` + * to still match a transition, even when `entering === []`. Contrast that + * with `entering: (state) => true` which only matches when a state is actually + * being entered. + */ + RegisteredHook.prototype._matchingNodes = function (nodes, criterion) { + if (criterion === true) + return nodes; + var matching = nodes.filter(function (node) { return matchState(node.state, criterion); }); + return matching.length ? matching : null; + }; + /** + * Gets the default match criteria (all `true`) + * + * Returns an object which has all the criteria match paths as keys and `true` as values, i.e.: + * + * ```js + * { + * to: true, + * from: true, + * entering: true, + * exiting: true, + * retained: true, + * } + */ + RegisteredHook.prototype._getDefaultMatchCriteria = function () { + return map(this.tranSvc._pluginapi._getPathTypes(), function () { return true; }); + }; + /** + * Gets matching nodes as [[IMatchingNodes]] + * + * Create a IMatchingNodes object from the TransitionHookTypes that is roughly equivalent to: + * + * ```js + * let matches: IMatchingNodes = { + * to: _matchingNodes([tail(treeChanges.to)], mc.to), + * from: _matchingNodes([tail(treeChanges.from)], mc.from), + * exiting: _matchingNodes(treeChanges.exiting, mc.exiting), + * retained: _matchingNodes(treeChanges.retained, mc.retained), + * entering: _matchingNodes(treeChanges.entering, mc.entering), + * }; + * ``` + */ + RegisteredHook.prototype._getMatchingNodes = function (treeChanges) { + var _this = this; + var criteria = extend(this._getDefaultMatchCriteria(), this.matchCriteria); + var paths = values(this.tranSvc._pluginapi._getPathTypes()); + return paths.reduce(function (mn, pathtype) { + // STATE scope criteria matches against every node in the path. + // TRANSITION scope criteria matches against only the last node in the path + var isStateHook = pathtype.scope === exports.TransitionHookScope.STATE; + var path = treeChanges[pathtype.name] || []; + var nodes = isStateHook ? path : [tail(path)]; + mn[pathtype.name] = _this._matchingNodes(nodes, criteria[pathtype.name]); + return mn; + }, {}); + }; + /** + * Determines if this hook's [[matchCriteria]] match the given [[TreeChanges]] + * + * @returns an IMatchingNodes object, or null. If an IMatchingNodes object is returned, its values + * are the matching [[PathNode]]s for each [[HookMatchCriterion]] (to, from, exiting, retained, entering) + */ + RegisteredHook.prototype.matches = function (treeChanges) { + var matches = this._getMatchingNodes(treeChanges); + // Check if all the criteria matched the TreeChanges object + var allMatched = values(matches).every(identity); + return allMatched ? matches : null; + }; + RegisteredHook.prototype.deregister = function () { + this.removeHookFromRegistry(this); + this._deregistered = true; + }; + return RegisteredHook; + }()); + /** @hidden Return a registration function of the requested type. */ + function makeEvent(registry, transitionService, eventType) { + // Create the object which holds the registered transition hooks. + var _registeredHooks = registry._registeredHooks = (registry._registeredHooks || {}); + var hooks = _registeredHooks[eventType.name] = []; + var removeHookFn = removeFrom(hooks); + // Create hook registration function on the IHookRegistry for the event + registry[eventType.name] = hookRegistrationFn; + function hookRegistrationFn(matchObject, callback, options) { + if (options === void 0) { options = {}; } + var registeredHook = new RegisteredHook(transitionService, eventType, callback, matchObject, removeHookFn, options); + hooks.push(registeredHook); + return registeredHook.deregister.bind(registeredHook); + } + return hookRegistrationFn; } /** - * Returns a new matcher for a pattern constructed by appending the path part and adding the - * search parameters of the specified pattern to this pattern. The current pattern is not - * modified. This can be understood as creating a pattern for URLs that are relative to (or - * suffixes of) the current pattern. - * - * ### Example - * The following two matchers are equivalent: - * ``` - * new UrlMatcher('/user/{id}?q').concat('/details?date'); - * new UrlMatcher('/user/{id}/details?q&date'); - * ``` - * - * @param {string} pattern The pattern to append. - * @return {UrlMatcher} A matcher for the concatenated pattern. - */ - UrlMatcher.prototype.concat = function (pattern) { - // Because order of search parameters is irrelevant, we can add our own search - // parameters to the end of the new pattern. Parse the new pattern by itself - // and then join the bits together, but it's much easier to do this on a string level. - return new UrlMatcher(this.sourcePath + pattern + this.sourceSearch); - }; - - UrlMatcher.prototype.toString = function () { - return this.source; - }; - + * @coreapi + * @module transition + */ /** for typedoc */ /** - * Tests the specified path against this matcher, and returns an object containing the captured - * parameter values, or null if the path does not match. The returned object contains the values - * of any search parameters that are mentioned in the pattern, but their value may be null if - * they are not present in `searchParams`. This means that search parameters are always treated - * as optional. + * This class returns applicable TransitionHooks for a specific Transition instance. * - * ### Example - * ``` - * new UrlMatcher('/user/{id}?q&r').exec('/user/bob', { x:'1', q:'hello' }); - * // returns { id:'bob', q:'hello', r:null } - * ``` + * Hooks ([[RegisteredHook]]) may be registered globally, e.g., $transitions.onEnter(...), or locally, e.g. + * myTransition.onEnter(...). The HookBuilder finds matching RegisteredHooks (where the match criteria is + * determined by the type of hook) + * + * The HookBuilder also converts RegisteredHooks objects to TransitionHook objects, which are used to run a Transition. + * + * The HookBuilder constructor is given the $transitions service and a Transition instance. Thus, a HookBuilder + * instance may only be used for one specific Transition object. (side note: the _treeChanges accessor is private + * in the Transition class, so we must also provide the Transition's _treeChanges) * - * @param {string} path The URL path to match, e.g. `$location.path()`. - * @param {Object} searchParams URL search parameters, e.g. `$location.search()`. - * @return {Object} The captured parameter values. */ - UrlMatcher.prototype.exec = function (path, searchParams) { - var m = this.regexp.exec(path); - if (!m) return null; - - var params = this.params, nTotal = params.length, - nPath = this.segments.length-1, - values = {}, i; - - if (nPath !== m.length - 1) throw new Error("Unbalanced capture group in route '" + this.source + "'"); - - for (i=0; i} An array of parameter names. Must be treated as read-only. If the - * pattern has no parameters, an empty array is returned. - */ - UrlMatcher.prototype.parameters = function () { - return this.params; - }; - - /** - * Creates a URL that matches this pattern by substituting the specified values - * for the path and search parameters. Null values for path parameters are - * treated as empty strings. - * - * ### Example - * ``` - * new UrlMatcher('/user/{id}?q').format({ id:'bob', q:'yes' }); - * // returns '/user/bob?q=yes' - * ``` - * - * @param {Object} values the values to substitute for the parameters in this pattern. - * @return {string} the formatted URL (path and optionally search part). - */ - UrlMatcher.prototype.format = function (values) { - var segments = this.segments, params = this.params; - if (!values) return segments.join(''); - - var nPath = segments.length-1, nTotal = params.length, - result = segments[0], i, search, value; - - for (i=0; i|any} + */ + ResolveContext.prototype.resolvePath = function (when, trans) { + var _this = this; + if (when === void 0) { when = "LAZY"; } + // This option determines which 'when' policy Resolvables we are about to fetch. + var whenOption = inArray(ALL_WHENS, when) ? when : "LAZY"; + // If the caller specified EAGER, only the EAGER Resolvables are fetched. + // if the caller specified LAZY, both EAGER and LAZY Resolvables are fetched.` + var matchedWhens = whenOption === resolvePolicies.when.EAGER ? EAGER_WHENS : ALL_WHENS; + // get the subpath to the state argument, if provided + trace.traceResolvePath(this._path, when, trans); + var matchesPolicy = function (acceptedVals, whenOrAsync) { + return function (resolvable) { + return inArray(acceptedVals, _this.getPolicy(resolvable)[whenOrAsync]); }; + }; + // Trigger all the (matching) Resolvables in the path + // Reduce all the "WAIT" Resolvables into an array + var promises = this._path.reduce(function (acc, node) { + var nodeResolvables = node.resolvables.filter(matchesPolicy(matchedWhens, 'when')); + var nowait = nodeResolvables.filter(matchesPolicy(['NOWAIT'], 'async')); + var wait = nodeResolvables.filter(not(matchesPolicy(['NOWAIT'], 'async'))); + // For the matching Resolvables, start their async fetch process. + var subContext = _this.subContext(node.state); + var getResult = function (r) { return r.get(subContext, trans) + .then(function (value) { return ({ token: r.token, value: value }); }); }; + nowait.forEach(getResult); + return acc.concat(wait.map(getResult)); + }, []); + // Wait for all the "WAIT" resolvables + return services.$q.all(promises); + }; + ResolveContext.prototype.injector = function () { + return this._injector || (this._injector = new UIInjectorImpl(this)); + }; + ResolveContext.prototype.findNode = function (resolvable) { + return find(this._path, function (node) { return inArray(node.resolvables, resolvable); }); + }; + /** + * Gets the async dependencies of a Resolvable + * + * Given a Resolvable, returns its dependencies as a Resolvable[] + */ + ResolveContext.prototype.getDependencies = function (resolvable) { + var _this = this; + var node = this.findNode(resolvable); + // Find which other resolvables are "visible" to the `resolvable` argument + // subpath stopping at resolvable's node, or the whole path (if the resolvable isn't in the path) + var subPath = PathUtils.subPath(this._path, function (x) { return x === node; }) || this._path; + var availableResolvables = subPath + .reduce(function (acc, _node) { return acc.concat(_node.resolvables); }, []) //all of subpath's resolvables + .filter(function (res) { return res !== resolvable; }); // filter out the `resolvable` argument + var getDependency = function (token) { + var matching = availableResolvables.filter(function (r) { return r.token === token; }); + if (matching.length) + return tail(matching); + var fromInjector = _this.injector().getNative(token); + if (isUndefined(fromInjector)) { + throw new Error("Could not find Dependency Injection token: " + stringify(token)); + } + return new Resolvable(token, function () { return fromInjector; }, [], fromInjector); + }; + return resolvable.deps.map(getDependency); + }; + return ResolveContext; + }()); + var UIInjectorImpl = /** @class */ (function () { + function UIInjectorImpl(context) { + this.context = context; + this.native = this.get(NATIVE_INJECTOR_TOKEN) || services.$injector; + } + UIInjectorImpl.prototype.get = function (token) { + var resolvable = this.context.getResolvable(token); + if (resolvable) { + if (this.context.getPolicy(resolvable).async === 'NOWAIT') { + return resolvable.get(this.context); + } + if (!resolvable.resolved) { + throw new Error("Resolvable async .get() not complete:" + stringify(resolvable.token)); + } + return resolvable.data; + } + return this.getNative(token); + }; + UIInjectorImpl.prototype.getAsync = function (token) { + var resolvable = this.context.getResolvable(token); + if (resolvable) + return resolvable.get(this.context); + return services.$q.when(this.native.get(token)); + }; + UIInjectorImpl.prototype.getNative = function (token) { + return this.native && this.native.get(token); + }; + return UIInjectorImpl; + }()); - this.$get = - [ '$location', '$rootScope', '$injector', - function ($location, $rootScope, $injector) { - // TODO: Optimize groups of rules with non-empty prefix into some sort of decision tree - function update(evt) { - if (evt && evt.defaultPrevented) return; - function check(rule) { - var handled = rule($injector, $location); - if (handled) { - if (isString(handled)) $location.replace().url(handled); - return true; - } - return false; - } - var n=rules.length, i; - for (i=0; i { + * var myResolveValue = trans.injector().get('myResolve'); + * // Inject a global service from the global/native injector (if it exists) + * var MyService = trans.injector().get('MyService'); + * }) + * ``` + * + * In some cases (such as `onBefore`), you may need access to some resolve data but it has not yet been fetched. + * You can use [[UIInjector.getAsync]] to get a promise for the data. + * #### Example: + * ```js + * .onBefore({}, trans => { + * return trans.injector().getAsync('myResolve').then(myResolveValue => + * return myResolveValue !== 'ABORT'; + * }); + * }); + * ``` + * + * If a `state` is provided, the injector that is returned will be limited to resolve values that the provided state has access to. + * This can be useful if both a parent state `foo` and a child state `foo.bar` have both defined a resolve such as `data`. + * #### Example: + * ```js + * .onEnter({ to: 'foo.bar' }, trans => { + * // returns result of `foo` state's `data` resolve + * // even though `foo.bar` also has a `data` resolve + * var fooData = trans.injector('foo').get('data'); + * }); + * ``` + * + * If you need resolve data from the exiting states, pass `'from'` as `pathName`. + * The resolve data from the `from` path will be returned. + * #### Example: + * ```js + * .onExit({ exiting: 'foo.bar' }, trans => { + * // Gets the resolve value of `data` from the exiting state. + * var fooData = trans.injector(null, 'foo.bar').get('data'); + * }); + * ``` + * + * + * @param state Limits the resolves provided to only the resolves the provided state has access to. + * @param pathName Default: `'to'`: Chooses the path for which to create the injector. Use this to access resolves for `exiting` states. + * + * @returns a [[UIInjector]] + */ + Transition.prototype.injector = function (state, pathName) { + if (pathName === void 0) { pathName = "to"; } + var path = this._treeChanges[pathName]; + if (state) + path = PathUtils.subPath(path, function (node) { return node.state === state || node.state.name === state; }); + return new ResolveContext(path).injector(); + }; + /** + * Gets all available resolve tokens (keys) + * + * This method can be used in conjunction with [[injector]] to inspect the resolve values + * available to the Transition. + * + * This returns all the tokens defined on [[StateDeclaration.resolve]] blocks, for the states + * in the Transition's [[TreeChanges.to]] path. + * + * #### Example: + * This example logs all resolve values + * ```js + * let tokens = trans.getResolveTokens(); + * tokens.forEach(token => console.log(token + " = " + trans.injector().get(token))); + * ``` + * + * #### Example: + * This example creates promises for each resolve value. + * This triggers fetches of resolves (if any have not yet been fetched). + * When all promises have all settled, it logs the resolve values. + * ```js + * let tokens = trans.getResolveTokens(); + * let promise = tokens.map(token => trans.injector().getAsync(token)); + * Promise.all(promises).then(values => console.log("Resolved values: " + values)); + * ``` + * + * Note: Angular 1 users whould use `$q.all()` + * + * @param pathname resolve context's path name (e.g., `to` or `from`) + * + * @returns an array of resolve tokens (keys) + */ + Transition.prototype.getResolveTokens = function (pathname) { + if (pathname === void 0) { pathname = "to"; } + return new ResolveContext(this._treeChanges[pathname]).getTokens(); + }; + /** + * Dynamically adds a new [[Resolvable]] (i.e., [[StateDeclaration.resolve]]) to this transition. + * + * #### Example: + * ```js + * transitionService.onBefore({}, transition => { + * transition.addResolvable({ + * token: 'myResolve', + * deps: ['MyService'], + * resolveFn: myService => myService.getData() + * }); + * }); + * ``` + * + * @param resolvable a [[ResolvableLiteral]] object (or a [[Resolvable]]) + * @param state the state in the "to path" which should receive the new resolve (otherwise, the root state) + */ + Transition.prototype.addResolvable = function (resolvable, state) { + if (state === void 0) { state = ""; } + resolvable = is(Resolvable)(resolvable) ? resolvable : new Resolvable(resolvable); + var stateName = (typeof state === "string") ? state : state.name; + var topath = this._treeChanges.to; + var targetNode = find(topath, function (node) { return node.state.name === stateName; }); + var resolveContext = new ResolveContext(topath); + resolveContext.addResolvables([resolvable], targetNode.state); + }; + /** + * Gets the transition from which this transition was redirected. + * + * If the current transition is a redirect, this method returns the transition that was redirected. + * + * #### Example: + * ```js + * let transitionA = $state.go('A').transition + * transitionA.onStart({}, () => $state.target('B')); + * $transitions.onSuccess({ to: 'B' }, (trans) => { + * trans.to().name === 'B'; // true + * trans.redirectedFrom() === transitionA; // true + * }); + * ``` + * + * @returns The previous Transition, or null if this Transition is not the result of a redirection + */ + Transition.prototype.redirectedFrom = function () { + return this._options.redirectedFrom || null; + }; + /** + * Gets the original transition in a redirect chain + * + * A transition might belong to a long chain of multiple redirects. + * This method walks the [[redirectedFrom]] chain back to the original (first) transition in the chain. + * + * #### Example: + * ```js + * // states + * registry.register({ name: 'A', redirectTo: 'B' }); + * registry.register({ name: 'B', redirectTo: 'C' }); + * registry.register({ name: 'C', redirectTo: 'D' }); + * registry.register({ name: 'D' }); + * + * let transitionA = $state.go('A').transition + * + * $transitions.onSuccess({ to: 'D' }, (trans) => { + * trans.to().name === 'D'; // true + * trans.redirectedFrom().to().name === 'C'; // true + * trans.originalTransition() === transitionA; // true + * trans.originalTransition().to().name === 'A'; // true + * }); + * ``` + * + * @returns The original Transition that started a redirect chain + */ + Transition.prototype.originalTransition = function () { + var rf = this.redirectedFrom(); + return (rf && rf.originalTransition()) || this; + }; + /** + * Get the transition options + * + * @returns the options for this Transition. + */ + Transition.prototype.options = function () { + return this._options; + }; + /** + * Gets the states being entered. + * + * @returns an array of states that will be entered during this transition. + */ + Transition.prototype.entering = function () { + return map(this._treeChanges.entering, prop('state')).map(stateSelf); + }; + /** + * Gets the states being exited. + * + * @returns an array of states that will be exited during this transition. + */ + Transition.prototype.exiting = function () { + return map(this._treeChanges.exiting, prop('state')).map(stateSelf).reverse(); + }; + /** + * Gets the states being retained. + * + * @returns an array of states that are already entered from a previous Transition, that will not be + * exited during this Transition + */ + Transition.prototype.retained = function () { + return map(this._treeChanges.retained, prop('state')).map(stateSelf); + }; + /** + * Get the [[ViewConfig]]s associated with this Transition + * + * Each state can define one or more views (template/controller), which are encapsulated as `ViewConfig` objects. + * This method fetches the `ViewConfigs` for a given path in the Transition (e.g., "to" or "entering"). + * + * @param pathname the name of the path to fetch views for: + * (`'to'`, `'from'`, `'entering'`, `'exiting'`, `'retained'`) + * @param state If provided, only returns the `ViewConfig`s for a single state in the path + * + * @returns a list of ViewConfig objects for the given path. + */ + Transition.prototype.views = function (pathname, state) { + if (pathname === void 0) { pathname = "entering"; } + var path = this._treeChanges[pathname]; + path = !state ? path : path.filter(propEq('state', state)); + return path.map(prop("views")).filter(identity).reduce(unnestR, []); + }; + Transition.prototype.treeChanges = function (pathname) { + return pathname ? this._treeChanges[pathname] : this._treeChanges; + }; + /** + * Creates a new transition that is a redirection of the current one. + * + * This transition can be returned from a [[TransitionService]] hook to + * redirect a transition to a new state and/or set of parameters. + * + * @internalapi + * + * @returns Returns a new [[Transition]] instance. + */ + Transition.prototype.redirect = function (targetState) { + var redirects = 1, trans = this; + while ((trans = trans.redirectedFrom()) != null) { + if (++redirects > 20) + throw new Error("Too many consecutive Transition redirects (20+)"); + } + var redirectOpts = { redirectedFrom: this, source: "redirect" }; + // If the original transition was caused by URL sync, then use { location: 'replace' } + // on the new transition (unless the target state explicitly specifies location: false). + // This causes the original url to be replaced with the url for the redirect target + // so the original url disappears from the browser history. + if (this.options().source === 'url' && targetState.options().location !== false) { + redirectOpts.location = 'replace'; + } + var newOptions = extend({}, this.options(), targetState.options(), redirectOpts); + targetState = targetState.withOptions(newOptions, true); + var newTransition = this.router.transitionService.create(this._treeChanges.from, targetState); + var originalEnteringNodes = this._treeChanges.entering; + var redirectEnteringNodes = newTransition._treeChanges.entering; + // --- Re-use resolve data from original transition --- + // When redirecting from a parent state to a child state where the parent parameter values haven't changed + // (because of the redirect), the resolves fetched by the original transition are still valid in the + // redirected transition. + // + // This allows you to define a redirect on a parent state which depends on an async resolve value. + // You can wait for the resolve, then redirect to a child state based on the result. + // The redirected transition does not have to re-fetch the resolve. + // --------------------------------------------------------- + var nodeIsReloading = function (reloadState) { return function (node) { + return reloadState && node.state.includes[reloadState.name]; + }; }; + // Find any "entering" nodes in the redirect path that match the original path and aren't being reloaded + var matchingEnteringNodes = PathUtils.matching(redirectEnteringNodes, originalEnteringNodes, PathUtils.nonDynamicParams) + .filter(not(nodeIsReloading(targetState.options().reloadState))); + // Use the existing (possibly pre-resolved) resolvables for the matching entering nodes. + matchingEnteringNodes.forEach(function (node, idx) { + node.resolvables = originalEnteringNodes[idx].resolvables; + }); + return newTransition; + }; + /** @hidden If a transition doesn't exit/enter any states, returns any [[Param]] whose value changed */ + Transition.prototype._changedParams = function () { + var tc = this._treeChanges; + /** Return undefined if it's not a "dynamic" transition, for the following reasons */ + // If user explicitly wants a reload + if (this._options.reload) + return undefined; + // If any states are exiting or entering + if (tc.exiting.length || tc.entering.length) + return undefined; + // If to/from path lengths differ + if (tc.to.length !== tc.from.length) + return undefined; + // If the to/from paths are different + var pathsDiffer = arrayTuples(tc.to, tc.from) + .map(function (tuple) { return tuple[0].state !== tuple[1].state; }) + .reduce(anyTrueR, false); + if (pathsDiffer) + return undefined; + // Find any parameter values that differ + var nodeSchemas = tc.to.map(function (node) { return node.paramSchema; }); + var _a = [tc.to, tc.from].map(function (path) { return path.map(function (x) { return x.paramValues; }); }), toValues = _a[0], fromValues = _a[1]; + var tuples = arrayTuples(nodeSchemas, toValues, fromValues); + return tuples.map(function (_a) { + var schema = _a[0], toVals = _a[1], fromVals = _a[2]; + return Param.changed(schema, toVals, fromVals); + }).reduce(unnestR, []); + }; + /** + * Returns true if the transition is dynamic. + * + * A transition is dynamic if no states are entered nor exited, but at least one dynamic parameter has changed. + * + * @returns true if the Transition is dynamic + */ + Transition.prototype.dynamic = function () { + var changes = this._changedParams(); + return !changes ? false : changes.map(function (x) { return x.dynamic; }).reduce(anyTrueR, false); + }; + /** + * Returns true if the transition is ignored. + * + * A transition is ignored if no states are entered nor exited, and no parameter values have changed. + * + * @returns true if the Transition is ignored. + */ + Transition.prototype.ignored = function () { + return !!this._ignoredReason(); + }; + /** @hidden */ + Transition.prototype._ignoredReason = function () { + var pending = this.router.globals.transition; + var reloadState = this._options.reloadState; + var same = function (pathA, pathB) { + if (pathA.length !== pathB.length) + return false; + var matching = PathUtils.matching(pathA, pathB); + return pathA.length === matching.filter(function (node) { return !reloadState || !node.state.includes[reloadState.name]; }).length; + }; + var newTC = this.treeChanges(); + var pendTC = pending && pending.treeChanges(); + if (pendTC && same(pendTC.to, newTC.to) && same(pendTC.exiting, newTC.exiting)) + return "SameAsPending"; + if (newTC.exiting.length === 0 && newTC.entering.length === 0 && same(newTC.from, newTC.to)) + return "SameAsCurrent"; + }; + /** + * Runs the transition + * + * This method is generally called from the [[StateService.transitionTo]] + * + * @internalapi + * + * @returns a promise for a successful transition. + */ + Transition.prototype.run = function () { + var _this = this; + var runAllHooks = TransitionHook.runAllHooks; + // Gets transition hooks array for the given phase + var getHooksFor = function (phase) { + return _this._hookBuilder.buildHooksForPhase(phase); + }; + // When the chain is complete, then resolve or reject the deferred + var transitionSuccess = function () { + trace.traceSuccess(_this.$to(), _this); + _this.success = true; + _this._deferred.resolve(_this.to()); + runAllHooks(getHooksFor(exports.TransitionHookPhase.SUCCESS)); + }; + var transitionError = function (reason) { + trace.traceError(reason, _this); + _this.success = false; + _this._deferred.reject(reason); + _this._error = reason; + runAllHooks(getHooksFor(exports.TransitionHookPhase.ERROR)); + }; + var runTransition = function () { + // Wait to build the RUN hook chain until the BEFORE hooks are done + // This allows a BEFORE hook to dynamically add additional RUN hooks via the Transition object. + var allRunHooks = getHooksFor(exports.TransitionHookPhase.RUN); + var done = function () { return services.$q.when(undefined); }; + return TransitionHook.invokeHooks(allRunHooks, done); + }; + var startTransition = function () { + var globals = _this.router.globals; + globals.lastStartedTransitionId = _this.$id; + globals.transition = _this; + globals.transitionHistory.enqueue(_this); + trace.traceTransitionStart(_this); + return services.$q.when(undefined); + }; + var allBeforeHooks = getHooksFor(exports.TransitionHookPhase.BEFORE); + TransitionHook.invokeHooks(allBeforeHooks, startTransition) + .then(runTransition) + .then(transitionSuccess, transitionError); + return this.promise; + }; + /** + * Checks if the Transition is valid + * + * @returns true if the Transition is valid + */ + Transition.prototype.valid = function () { + return !this.error() || this.success !== undefined; + }; + /** + * Aborts this transition + * + * Imperative API to abort a Transition. + * This only applies to Transitions that are not yet complete. + */ + Transition.prototype.abort = function () { + // Do not set flag if the transition is already complete + if (isUndefined(this.success)) { + this._aborted = true; + } + }; + /** + * The Transition error reason. + * + * If the transition is invalid (and could not be run), returns the reason the transition is invalid. + * If the transition was valid and ran, but was not successful, returns the reason the transition failed. + * + * @returns an error message explaining why the transition is invalid, or the reason the transition failed. + */ + Transition.prototype.error = function () { + var state = this.$to(); + if (state.self.abstract) + return "Cannot transition to abstract state '" + state.name + "'"; + var paramDefs = state.parameters(), values$$1 = this.params(); + var invalidParams = paramDefs.filter(function (param) { return !param.validates(values$$1[param.id]); }); + if (invalidParams.length) { + return "Param values not valid for state '" + state.name + "'. Invalid params: [ " + invalidParams.map(function (param) { return param.id; }).join(', ') + " ]"; + } + if (this.success === false) + return this._error; + }; + /** + * A string representation of the Transition + * + * @returns A string representation of the Transition + */ + Transition.prototype.toString = function () { + var fromStateOrName = this.from(); + var toStateOrName = this.to(); + var avoidEmptyHash = function (params) { + return (params["#"] !== null && params["#"] !== undefined) ? params : omit(params, ["#"]); + }; + // (X) means the to state is invalid. + var id = this.$id, from = isObject(fromStateOrName) ? fromStateOrName.name : fromStateOrName, fromParams = stringify(avoidEmptyHash(this._treeChanges.from.map(prop('paramValues')).reduce(mergeR, {}))), toValid = this.valid() ? "" : "(X) ", to = isObject(toStateOrName) ? toStateOrName.name : toStateOrName, toParams = stringify(avoidEmptyHash(this.params())); + return "Transition#" + id + "( '" + from + "'" + fromParams + " -> " + toValid + "'" + to + "'" + toParams + " )"; + }; + /** @hidden */ + Transition.diToken = Transition; + return Transition; + }()); - $rootScope.$on('$locationChangeSuccess', update); - - return { - sync: function () { - update(); - } - }; - }]; + /** + * Functions that manipulate strings + * + * Although these functions are exported, they are subject to change without notice. + * + * @module common_strings + */ /** */ + /** + * Returns a string shortened to a maximum length + * + * If the string is already less than the `max` length, return the string. + * Else return the string, shortened to `max - 3` and append three dots ("..."). + * + * @param max the maximum length of the string to return + * @param str the input string + */ + function maxLength(max, str) { + if (str.length <= max) + return str; + return str.substr(0, max - 3) + "..."; + } + /** + * Returns a string, with spaces added to the end, up to a desired str length + * + * If the string is already longer than the desired length, return the string. + * Else returns the string, with extra spaces on the end, such that it reaches `length` characters. + * + * @param length the desired length of the string to return + * @param str the input string + */ + function padString(length, str) { + while (str.length < length) + str += " "; + return str; + } + function kebobString(camelCase) { + return camelCase + .replace(/^([A-Z])/, function ($1) { return $1.toLowerCase(); }) // replace first char + .replace(/([A-Z])/g, function ($1) { return "-" + $1.toLowerCase(); }); // replace rest + } + function functionToString(fn) { + var fnStr = fnToString(fn); + var namedFunctionMatch = fnStr.match(/^(function [^ ]+\([^)]*\))/); + var toStr = namedFunctionMatch ? namedFunctionMatch[1] : fnStr; + var fnName = fn['name'] || ""; + if (fnName && toStr.match(/function \(/)) { + return 'function ' + fnName + toStr.substr(9); + } + return toStr; + } + function fnToString(fn) { + var _fn = isArray(fn) ? fn.slice(-1)[0] : fn; + return _fn && _fn.toString() || "undefined"; + } + var stringifyPatternFn = null; + var stringifyPattern = function (value) { + var isRejection = Rejection.isRejectionPromise; + stringifyPatternFn = stringifyPatternFn || pattern([ + [not(isDefined), val("undefined")], + [isNull, val("null")], + [isPromise, val("[Promise]")], + [isRejection, function (x) { return x._transitionRejection.toString(); }], + [is(Rejection), invoke("toString")], + [is(Transition), invoke("toString")], + [is(Resolvable), invoke("toString")], + [isInjectable, functionToString], + [val(true), identity] + ]); + return stringifyPatternFn(value); + }; + function stringify(o) { + var seen = []; + function format(val$$1) { + if (isObject(val$$1)) { + if (seen.indexOf(val$$1) !== -1) + return '[circular ref]'; + seen.push(val$$1); + } + return stringifyPattern(val$$1); + } + return JSON.stringify(o, function (key, val$$1) { return format(val$$1); }).replace(/\\"/g, '"'); + } + /** Returns a function that splits a string on a character or substring */ + var beforeAfterSubstr = function (char) { return function (str) { + if (!str) + return ["", ""]; + var idx = str.indexOf(char); + if (idx === -1) + return [str, ""]; + return [str.substr(0, idx), str.substr(idx + 1)]; + }; }; + var hostRegex = new RegExp('^(?:[a-z]+:)?//[^/]+/'); + var stripFile = function (str) { return str.replace(/\/[^/]*$/, ''); }; + var splitHash = beforeAfterSubstr("#"); + var splitQuery = beforeAfterSubstr("?"); + var splitEqual = beforeAfterSubstr("="); + var trimHashVal = function (str) { return str ? str.replace(/^#/, "") : ""; }; + /** + * Splits on a delimiter, but returns the delimiters in the array + * + * #### Example: + * ```js + * var splitOnSlashes = splitOnDelim('/'); + * splitOnSlashes("/foo"); // ["/", "foo"] + * splitOnSlashes("/foo/"); // ["/", "foo", "/"] + * ``` + */ + function splitOnDelim(delim) { + var re = new RegExp("(" + delim + ")", "g"); + return function (str) { + return str.split(re).filter(identity); + }; } - angular.module('ui.router.router').provider('$urlRouter', $UrlRouterProvider); + /** + * Reduce fn that joins neighboring strings + * + * Given an array of strings, returns a new array + * where all neighboring strings have been joined. + * + * #### Example: + * ```js + * let arr = ["foo", "bar", 1, "baz", "", "qux" ]; + * arr.reduce(joinNeighborsR, []) // ["foobar", 1, "bazqux" ] + * ``` + */ + function joinNeighborsR(acc, x) { + if (isString(tail(acc)) && isString(x)) + return acc.slice(0, -1).concat(tail(acc) + x); + return pushR(acc, x); + } - $StateProvider.$inject = ['$urlRouterProvider', '$urlMatcherFactoryProvider', '$locationProvider']; - function $StateProvider( $urlRouterProvider, $urlMatcherFactory, $locationProvider) { + /** @module common */ /** for typedoc */ - var root, states = {}, $state, queue = {}, abstractKey = 'abstract'; + /** + * @coreapi + * @module params + */ + /** */ + /** + * A registry for parameter types. + * + * This registry manages the built-in (and custom) parameter types. + * + * The built-in parameter types are: + * + * - [[string]] + * - [[path]] + * - [[query]] + * - [[hash]] + * - [[int]] + * - [[bool]] + * - [[date]] + * - [[json]] + * - [[any]] + */ + var ParamTypes = /** @class */ (function () { + /** @internalapi */ + function ParamTypes() { + /** @hidden */ + this.enqueue = true; + /** @hidden */ + this.typeQueue = []; + /** @internalapi */ + this.defaultTypes = pick(ParamTypes.prototype, ["hash", "string", "query", "path", "int", "bool", "date", "json", "any"]); + // Register default types. Store them in the prototype of this.types. + var makeType = function (definition, name) { + return new ParamType(extend({ name: name }, definition)); + }; + this.types = inherit(map(this.defaultTypes, makeType), {}); + } + /** @internalapi */ + ParamTypes.prototype.dispose = function () { + this.types = {}; + }; + /** + * Registers a parameter type + * + * End users should call [[UrlMatcherFactory.type]], which delegates to this method. + */ + ParamTypes.prototype.type = function (name, definition, definitionFn) { + if (!isDefined(definition)) + return this.types[name]; + if (this.types.hasOwnProperty(name)) + throw new Error("A type named '" + name + "' has already been defined."); + this.types[name] = new ParamType(extend({ name: name }, definition)); + if (definitionFn) { + this.typeQueue.push({ name: name, def: definitionFn }); + if (!this.enqueue) + this._flushTypeQueue(); + } + return this; + }; + /** @internalapi */ + ParamTypes.prototype._flushTypeQueue = function () { + while (this.typeQueue.length) { + var type = this.typeQueue.shift(); + if (type.pattern) + throw new Error("You cannot override a type's .pattern at runtime."); + extend(this.types[type.name], services.$injector.invoke(type.def)); + } + }; + return ParamTypes; + }()); + /** @hidden */ + function initDefaultTypes() { + var makeDefaultType = function (def) { + var valToString = function (val$$1) { + return val$$1 != null ? val$$1.toString() : val$$1; + }; + var defaultTypeBase = { + encode: valToString, + decode: valToString, + is: is(String), + pattern: /.*/, + equals: function (a, b) { return a == b; }, + }; + return extend({}, defaultTypeBase, def); + }; + // Default Parameter Type Definitions + extend(ParamTypes.prototype, { + string: makeDefaultType({}), + path: makeDefaultType({ + pattern: /[^/]*/, + }), + query: makeDefaultType({}), + hash: makeDefaultType({ + inherit: false, + }), + int: makeDefaultType({ + decode: function (val$$1) { return parseInt(val$$1, 10); }, + is: function (val$$1) { + return !isNullOrUndefined(val$$1) && this.decode(val$$1.toString()) === val$$1; + }, + pattern: /-?\d+/, + }), + bool: makeDefaultType({ + encode: function (val$$1) { return val$$1 && 1 || 0; }, + decode: function (val$$1) { return parseInt(val$$1, 10) !== 0; }, + is: is(Boolean), + pattern: /0|1/, + }), + date: makeDefaultType({ + encode: function (val$$1) { + return !this.is(val$$1) ? undefined : [ + val$$1.getFullYear(), + ('0' + (val$$1.getMonth() + 1)).slice(-2), + ('0' + val$$1.getDate()).slice(-2), + ].join("-"); + }, + decode: function (val$$1) { + if (this.is(val$$1)) + return val$$1; + var match = this.capture.exec(val$$1); + return match ? new Date(match[1], match[2] - 1, match[3]) : undefined; + }, + is: function (val$$1) { return val$$1 instanceof Date && !isNaN(val$$1.valueOf()); }, + equals: function (l, r) { + return ['getFullYear', 'getMonth', 'getDate'] + .reduce(function (acc, fn) { return acc && l[fn]() === r[fn](); }, true); + }, + pattern: /[0-9]{4}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[1-2][0-9]|3[0-1])/, + capture: /([0-9]{4})-(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[0-1])/, + }), + json: makeDefaultType({ + encode: toJson, + decode: fromJson, + is: is(Object), + equals: equals, + pattern: /[^/]*/, + }), + // does not encode/decode + any: makeDefaultType({ + encode: identity, + decode: identity, + is: function () { return true; }, + equals: equals, + }), + }); + } + initDefaultTypes(); - // Builds state properties from definition passed to registerState() - var stateBuilder = { - - // Derive parent state from a hierarchical name only if 'parent' is not explicitly defined. - // state.children = []; - // if (parent) parent.children.push(state); - parent: function(state) { - if (isDefined(state.parent) && state.parent) return findState(state.parent); - // regex matches any valid composite state name - // would match "contact.list" but not "contacts" - var compositeName = /^(.+)\.[^.]+$/.exec(state.name); - return compositeName ? findState(compositeName[1]) : root; - }, - - // inherit 'data' from parent and override by own values (if any) - data: function(state) { - if (state.parent && state.parent.data) { - state.data = state.self.data = extend({}, state.parent.data, state.data); + /** + * @coreapi + * @module params + */ + /** */ + /** @internalapi */ + var StateParams = /** @class */ (function () { + function StateParams(params) { + if (params === void 0) { params = {}; } + extend(this, params); + } + /** + * Merges a set of parameters with all parameters inherited between the common parents of the + * current state and a given destination state. + * + * @param {Object} newParams The set of parameters which will be composited with inherited params. + * @param {Object} $current Internal definition of object representing the current state. + * @param {Object} $to Internal definition of object representing state to transition to. + */ + StateParams.prototype.$inherit = function (newParams, $current, $to) { + var parents = ancestors($current, $to), parentParams, inherited = {}, inheritList = []; + for (var i in parents) { + if (!parents[i] || !parents[i].params) + continue; + parentParams = Object.keys(parents[i].params); + if (!parentParams.length) + continue; + for (var j in parentParams) { + if (inheritList.indexOf(parentParams[j]) >= 0) + continue; + inheritList.push(parentParams[j]); + inherited[parentParams[j]] = this[parentParams[j]]; } - return state.data; - }, - - // Build a URLMatcher if necessary, either via a relative or absolute URL - url: function(state) { - var url = state.url; - - if (isString(url)) { - if (url.charAt(0) == '^') { - return $urlMatcherFactory.compile(url.substring(1)); - } - return (state.parent.navigable || root).url.concat(url); - } - - if ($urlMatcherFactory.isMatcher(url) || url == null) { - return url; - } - throw new Error("Invalid url '" + url + "' in state '" + state + "'"); - }, - - // Keep track of the closest ancestor state that has a URL (i.e. is navigable) - navigable: function(state) { - return state.url ? state : (state.parent ? state.parent.navigable : null); - }, - - // Derive parameters for this state and ensure they're a super-set of parent's parameters - params: function(state) { - if (!state.params) { - return state.url ? state.url.parameters() : state.parent.params; - } - if (!isArray(state.params)) throw new Error("Invalid params in state '" + state + "'"); - if (state.url) throw new Error("Both params and url specicified in state '" + state + "'"); - return state.params; - }, - - // If there is no explicit multi-view configuration, make one up so we don't have - // to handle both cases in the view directive later. Note that having an explicit - // 'views' property will mean the default unnamed view properties are ignored. This - // is also a good time to resolve view names to absolute names, so everything is a - // straight lookup at link time. - views: function(state) { - var views = {}; - - forEach(isDefined(state.views) ? state.views : { '': state }, function (view, name) { - if (name.indexOf('@') < 0) name += '@' + state.parent.name; - views[name] = view; - }); - return views; - }, - - ownParams: function(state) { - if (!state.parent) { - return state.params; - } - var paramNames = {}; forEach(state.params, function (p) { paramNames[p] = true; }); - - forEach(state.parent.params, function (p) { - if (!paramNames[p]) { - throw new Error("Missing required parameter '" + p + "' in state '" + state.name + "'"); - } - paramNames[p] = false; - }); - var ownParams = []; - - forEach(paramNames, function (own, p) { - if (own) ownParams.push(p); - }); - return ownParams; - }, - - // Keep a full path from the root down to this state as this is needed for state activation. - path: function(state) { - return state.parent ? state.parent.path.concat(state) : []; // exclude root from path - }, - - // Speed up $state.contains() as it's used a lot - includes: function(state) { - var includes = state.parent ? extend({}, state.parent.includes) : {}; - includes[state.name] = true; - return includes; - }, - - $delegates: {} + } + return extend({}, inherited, newParams); }; - function isRelative(stateName) { - return stateName.indexOf(".") === 0 || stateName.indexOf("^") === 0; + return StateParams; + }()); + + /** @module path */ /** for typedoc */ + + /** @module resolve */ /** for typedoc */ + + /** @module state */ /** for typedoc */ + var parseUrl = function (url) { + if (!isString(url)) + return false; + var root$$1 = url.charAt(0) === '^'; + return { val: root$$1 ? url.substring(1) : url, root: root$$1 }; + }; + function nameBuilder(state) { + return state.name; + } + function selfBuilder(state) { + state.self.$$state = function () { return state; }; + return state.self; + } + function dataBuilder(state) { + if (state.parent && state.parent.data) { + state.data = state.self.data = inherit(state.parent.data, state.data); } + return state.data; + } + var getUrlBuilder = function ($urlMatcherFactoryProvider, root$$1) { + return function urlBuilder(state) { + var stateDec = state; + // For future states, i.e., states whose name ends with `.**`, + // match anything that starts with the url prefix + if (stateDec && stateDec.url && stateDec.name && stateDec.name.match(/\.\*\*$/)) { + stateDec.url += "{remainder:any}"; // match any path (.*) + } + var parsed = parseUrl(stateDec.url), parent = state.parent; + var url = !parsed ? stateDec.url : $urlMatcherFactoryProvider.compile(parsed.val, { + params: state.params || {}, + paramMap: function (paramConfig, isSearch) { + if (stateDec.reloadOnSearch === false && isSearch) + paramConfig = extend(paramConfig || {}, { dynamic: true }); + return paramConfig; + } + }); + if (!url) + return null; + if (!$urlMatcherFactoryProvider.isMatcher(url)) + throw new Error("Invalid url '" + url + "' in state '" + state + "'"); + return (parsed && parsed.root) ? url : ((parent && parent.navigable) || root$$1()).url.append(url); + }; + }; + var getNavigableBuilder = function (isRoot) { + return function navigableBuilder(state) { + return !isRoot(state) && state.url ? state : (state.parent ? state.parent.navigable : null); + }; + }; + var getParamsBuilder = function (paramFactory) { + return function paramsBuilder(state) { + var makeConfigParam = function (config, id) { return paramFactory.fromConfig(id, null, config); }; + var urlParams = (state.url && state.url.parameters({ inherit: false })) || []; + var nonUrlParams = values(mapObj(omit(state.params || {}, urlParams.map(prop('id'))), makeConfigParam)); + return urlParams.concat(nonUrlParams).map(function (p) { return [p.id, p]; }).reduce(applyPairs, {}); + }; + }; + function pathBuilder(state) { + return state.parent ? state.parent.path.concat(state) : /*root*/ [state]; + } + function includesBuilder(state) { + var includes = state.parent ? extend({}, state.parent.includes) : {}; + includes[state.name] = true; + return includes; + } + /** + * This is a [[StateBuilder.builder]] function for the `resolve:` block on a [[StateDeclaration]]. + * + * When the [[StateBuilder]] builds a [[StateObject]] object from a raw [[StateDeclaration]], this builder + * validates the `resolve` property and converts it to a [[Resolvable]] array. + * + * resolve: input value can be: + * + * { + * // analyzed but not injected + * myFooResolve: function() { return "myFooData"; }, + * + * // function.toString() parsed, "DependencyName" dep as string (not min-safe) + * myBarResolve: function(DependencyName) { return DependencyName.fetchSomethingAsPromise() }, + * + * // Array split; "DependencyName" dep as string + * myBazResolve: [ "DependencyName", function(dep) { return dep.fetchSomethingAsPromise() }, + * + * // Array split; DependencyType dep as token (compared using ===) + * myQuxResolve: [ DependencyType, function(dep) { return dep.fetchSometingAsPromise() }, + * + * // val.$inject used as deps + * // where: + * // corgeResolve.$inject = ["DependencyName"]; + * // function corgeResolve(dep) { dep.fetchSometingAsPromise() } + * // then "DependencyName" dep as string + * myCorgeResolve: corgeResolve, + * + * // inject service by name + * // When a string is found, desugar creating a resolve that injects the named service + * myGraultResolve: "SomeService" + * } + * + * or: + * + * [ + * new Resolvable("myFooResolve", function() { return "myFooData" }), + * new Resolvable("myBarResolve", function(dep) { return dep.fetchSomethingAsPromise() }, [ "DependencyName" ]), + * { provide: "myBazResolve", useFactory: function(dep) { dep.fetchSomethingAsPromise() }, deps: [ "DependencyName" ] } + * ] + */ + function resolvablesBuilder(state) { + /** convert resolve: {} and resolvePolicy: {} objects to an array of tuples */ + var objects2Tuples = function (resolveObj, resolvePolicies) { + return Object.keys(resolveObj || {}).map(function (token) { return ({ token: token, val: resolveObj[token], deps: undefined, policy: resolvePolicies[token] }); }); + }; + /** fetch DI annotations from a function or ng1-style array */ + var annotate = function (fn) { + var $injector = services.$injector; + // ng1 doesn't have an $injector until runtime. + // If the $injector doesn't exist, use "deferred" literal as a + // marker indicating they should be annotated when runtime starts + return fn['$inject'] || ($injector && $injector.annotate(fn, $injector.strictDi)) || "deferred"; + }; + /** true if the object has both `token` and `resolveFn`, and is probably a [[ResolveLiteral]] */ + var isResolveLiteral = function (obj) { return !!(obj.token && obj.resolveFn); }; + /** true if the object looks like a provide literal, or a ng2 Provider */ + var isLikeNg2Provider = function (obj) { return !!((obj.provide || obj.token) && (obj.useValue || obj.useFactory || obj.useExisting || obj.useClass)); }; + /** true if the object looks like a tuple from obj2Tuples */ + var isTupleFromObj = function (obj) { return !!(obj && obj.val && (isString(obj.val) || isArray(obj.val) || isFunction(obj.val))); }; + /** extracts the token from a Provider or provide literal */ + var token = function (p) { return p.provide || p.token; }; + /** Given a literal resolve or provider object, returns a Resolvable */ + var literal2Resolvable = pattern([ + [prop('resolveFn'), function (p) { return new Resolvable(token(p), p.resolveFn, p.deps, p.policy); }], + [prop('useFactory'), function (p) { return new Resolvable(token(p), p.useFactory, (p.deps || p.dependencies), p.policy); }], + [prop('useClass'), function (p) { return new Resolvable(token(p), function () { return new p.useClass(); }, [], p.policy); }], + [prop('useValue'), function (p) { return new Resolvable(token(p), function () { return p.useValue; }, [], p.policy, p.useValue); }], + [prop('useExisting'), function (p) { return new Resolvable(token(p), identity, [p.useExisting], p.policy); }], + ]); + var tuple2Resolvable = pattern([ + [pipe(prop("val"), isString), function (tuple) { return new Resolvable(tuple.token, identity, [tuple.val], tuple.policy); }], + [pipe(prop("val"), isArray), function (tuple) { return new Resolvable(tuple.token, tail(tuple.val), tuple.val.slice(0, -1), tuple.policy); }], + [pipe(prop("val"), isFunction), function (tuple) { return new Resolvable(tuple.token, tuple.val, annotate(tuple.val), tuple.policy); }], + ]); + var item2Resolvable = pattern([ + [is(Resolvable), function (r) { return r; }], + [isResolveLiteral, literal2Resolvable], + [isLikeNg2Provider, literal2Resolvable], + [isTupleFromObj, tuple2Resolvable], + [val(true), function (obj) { throw new Error("Invalid resolve value: " + stringify(obj)); }] + ]); + // If resolveBlock is already an array, use it as-is. + // Otherwise, assume it's an object and convert to an Array of tuples + var decl = state.resolve; + var items = isArray(decl) ? decl : objects2Tuples(decl, state.resolvePolicy || {}); + return items.map(item2Resolvable); + } + /** + * @internalapi A internal global service + * + * StateBuilder is a factory for the internal [[StateObject]] objects. + * + * When you register a state with the [[StateRegistry]], you register a plain old javascript object which + * conforms to the [[StateDeclaration]] interface. This factory takes that object and builds the corresponding + * [[StateObject]] object, which has an API and is used internally. + * + * Custom properties or API may be added to the internal [[StateObject]] object by registering a decorator function + * using the [[builder]] method. + */ + var StateBuilder = /** @class */ (function () { + function StateBuilder(matcher, urlMatcherFactory) { + this.matcher = matcher; + var self = this; + var root$$1 = function () { return matcher.find(""); }; + var isRoot = function (state) { return state.name === ""; }; + function parentBuilder(state) { + if (isRoot(state)) + return null; + return matcher.find(self.parentName(state)) || root$$1(); + } + this.builders = { + name: [nameBuilder], + self: [selfBuilder], + parent: [parentBuilder], + data: [dataBuilder], + // Build a URLMatcher if necessary, either via a relative or absolute URL + url: [getUrlBuilder(urlMatcherFactory, root$$1)], + // Keep track of the closest ancestor state that has a URL (i.e. is navigable) + navigable: [getNavigableBuilder(isRoot)], + params: [getParamsBuilder(urlMatcherFactory.paramFactory)], + // Each framework-specific ui-router implementation should define its own `views` builder + // e.g., src/ng1/statebuilders/views.ts + views: [], + // Keep a full path from the root down to this state as this is needed for state activation. + path: [pathBuilder], + // Speed up $state.includes() as it's used a lot + includes: [includesBuilder], + resolvables: [resolvablesBuilder] + }; + } + /** + * Registers a [[BuilderFunction]] for a specific [[StateObject]] property (e.g., `parent`, `url`, or `path`). + * More than one BuilderFunction can be registered for a given property. + * + * The BuilderFunction(s) will be used to define the property on any subsequently built [[StateObject]] objects. + * + * @param name The name of the State property being registered for. + * @param fn The BuilderFunction which will be used to build the State property + * @returns a function which deregisters the BuilderFunction + */ + StateBuilder.prototype.builder = function (name, fn) { + var builders = this.builders; + var array = builders[name] || []; + // Backwards compat: if only one builder exists, return it, else return whole arary. + if (isString(name) && !isDefined(fn)) + return array.length > 1 ? array : array[0]; + if (!isString(name) || !isFunction(fn)) + return; + builders[name] = array; + builders[name].push(fn); + return function () { return builders[name].splice(builders[name].indexOf(fn, 1)) && null; }; + }; + /** + * Builds all of the properties on an essentially blank State object, returning a State object which has all its + * properties and API built. + * + * @param state an uninitialized State object + * @returns the built State object + */ + StateBuilder.prototype.build = function (state) { + var _a = this, matcher = _a.matcher, builders = _a.builders; + var parent = this.parentName(state); + if (parent && !matcher.find(parent, undefined, false)) { + return null; + } + for (var key in builders) { + if (!builders.hasOwnProperty(key)) + continue; + var chain = builders[key].reduce(function (parentFn, step) { return function (_state) { return step(_state, parentFn); }; }, noop$1); + state[key] = chain(state); + } + return state; + }; + StateBuilder.prototype.parentName = function (state) { + // name = 'foo.bar.baz.**' + var name = state.name || ""; + // segments = ['foo', 'bar', 'baz', '.**'] + var segments = name.split('.'); + // segments = ['foo', 'bar', 'baz'] + var lastSegment = segments.pop(); + // segments = ['foo', 'bar'] (ignore .** segment for future states) + if (lastSegment === '**') + segments.pop(); + if (segments.length) { + if (state.parent) { + throw new Error("States that specify the 'parent:' property should not have a '.' in their name (" + name + ")"); + } + // 'foo.bar' + return segments.join("."); + } + if (!state.parent) + return ""; + return isString(state.parent) ? state.parent : state.parent.name; + }; + StateBuilder.prototype.name = function (state) { + var name = state.name; + if (name.indexOf('.') !== -1 || !state.parent) + return name; + var parentName = isString(state.parent) ? state.parent : state.parent.name; + return parentName ? parentName + "." + name : name; + }; + return StateBuilder; + }()); - function findState(stateOrName, base) { - var isStr = isString(stateOrName), - name = isStr ? stateOrName : stateOrName.name, - path = isRelative(name); - - if (path) { - if (!base) throw new Error("No reference point given for path '" + name + "'"); - var rel = name.split("."), i = 0, pathLength = rel.length, current = base; - + /** @module state */ /** for typedoc */ + var StateMatcher = /** @class */ (function () { + function StateMatcher(_states) { + this._states = _states; + } + StateMatcher.prototype.isRelative = function (stateName) { + stateName = stateName || ""; + return stateName.indexOf(".") === 0 || stateName.indexOf("^") === 0; + }; + StateMatcher.prototype.find = function (stateOrName, base, matchGlob) { + if (matchGlob === void 0) { matchGlob = true; } + if (!stateOrName && stateOrName !== "") + return undefined; + var isStr = isString(stateOrName); + var name = isStr ? stateOrName : stateOrName.name; + if (this.isRelative(name)) + name = this.resolvePath(name, base); + var state = this._states[name]; + if (state && (isStr || (!isStr && (state === stateOrName || state.self === stateOrName)))) { + return state; + } + else if (isStr && matchGlob) { + var _states = values(this._states); + var matches = _states.filter(function (state) { + return state.__stateObjectCache.nameGlob && + state.__stateObjectCache.nameGlob.matches(name); + }); + if (matches.length > 1) { + console.log("stateMatcher.find: Found multiple matches for " + name + " using glob: ", matches.map(function (match) { return match.name; })); + } + return matches[0]; + } + return undefined; + }; + StateMatcher.prototype.resolvePath = function (name, base) { + if (!base) + throw new Error("No reference point given for path '" + name + "'"); + var baseState = this.find(base); + var splitName = name.split("."), i = 0, pathLength = splitName.length, current = baseState; for (; i < pathLength; i++) { - if (rel[i] === "" && i === 0) { - current = base; + if (splitName[i] === "" && i === 0) { + current = baseState; continue; } - if (rel[i] === "^") { - if (!current.parent) throw new Error("Path '" + name + "' not valid for state '" + base.name + "'"); + if (splitName[i] === "^") { + if (!current.parent) + throw new Error("Path '" + name + "' not valid for state '" + baseState.name + "'"); current = current.parent; continue; } break; } - rel = rel.slice(i).join("."); - name = current.name + (current.name && rel ? "." : "") + rel; - } - var state = states[name]; + var relName = splitName.slice(i).join("."); + return current.name + (current.name && relName ? "." : "") + relName; + }; + return StateMatcher; + }()); - if (state && (isStr || (!isStr && (state === stateOrName || state.self === stateOrName)))) { - return state; - } - return undefined; + /** @module state */ /** for typedoc */ + /** @internalapi */ + var StateQueueManager = /** @class */ (function () { + function StateQueueManager($registry, $urlRouter, states, builder, listeners) { + this.$registry = $registry; + this.$urlRouter = $urlRouter; + this.states = states; + this.builder = builder; + this.listeners = listeners; + this.queue = []; + this.matcher = $registry.matcher; } - - function queueState(parentName, state) { - if (!queue[parentName]) { - queue[parentName] = []; - } - queue[parentName].push(state); - } - - function registerState(state) { - // Wrap a new object around the state so we can store our private details easily. - state = inherit(state, { - self: state, - resolve: state.resolve || {}, - toString: function() { return this.name; } - }); - + /** @internalapi */ + StateQueueManager.prototype.dispose = function () { + this.queue = []; + }; + StateQueueManager.prototype.register = function (stateDecl) { + var queue = this.queue; + var state = StateObject.create(stateDecl); var name = state.name; - if (!isString(name) || name.indexOf('@') >= 0) throw new Error("State must have a valid name"); - if (states.hasOwnProperty(name)) throw new Error("State '" + name + "'' is already defined"); - - // Get parent name - var parentName = (name.indexOf('.') !== -1) ? name.substring(0, name.lastIndexOf('.')) - : (isString(state.parent)) ? state.parent - : ''; - - // If parent is not registered yet, add state to queue and register later - if (parentName && !states[parentName]) { - return queueState(parentName, state.self); - } - - for (var key in stateBuilder) { - if (isFunction(stateBuilder[key])) state[key] = stateBuilder[key](state, stateBuilder.$delegates[key]); - } - states[name] = state; - - // Register the state in the global state list and with $urlRouter if necessary. - if (!state[abstractKey] && state.url) { - $urlRouterProvider.when(state.url, ['$match', '$stateParams', function ($match, $stateParams) { - if ($state.$current.navigable != state || !equalForKeys($match, $stateParams)) { - $state.transitionTo(state, $match, { location: false }); - } - }]); - } - - // Register any queued children - if (queue[name]) { - for (var i = 0; i < queue[name].length; i++) { - registerState(queue[name][i]); - } - } - + if (!isString(name)) + throw new Error("State must have a valid name"); + if (this.states.hasOwnProperty(name) || inArray(queue.map(prop('name')), name)) + throw new Error("State '" + name + "' is already defined"); + queue.push(state); + this.flush(); return state; - } - - - // Implicit root state that is always active - root = registerState({ - name: '', - url: '^', - views: null, - 'abstract': true - }); - root.navigable = null; - - - // .decorator() - // .decorator(name) - // .decorator(name, function) - this.decorator = decorator; - function decorator(name, func) { - /*jshint validthis: true */ - if (isString(name) && !isDefined(func)) { - return stateBuilder[name]; - } - if (!isFunction(func) || !isString(name)) { - return this; - } - if (stateBuilder[name] && !stateBuilder.$delegates[name]) { - stateBuilder.$delegates[name] = stateBuilder[name]; - } - stateBuilder[name] = func; - return this; - } - - // .state(state) - // .state(name, state) - this.state = state; - function state(name, definition) { - /*jshint validthis: true */ - if (isObject(name)) definition = name; - else definition.name = name; - registerState(definition); - return this; - } - - // $urlRouter is injected just to ensure it gets instantiated - this.$get = $get; - $get.$inject = ['$rootScope', '$q', '$view', '$injector', '$resolve', '$stateParams', '$location', '$urlRouter']; - function $get( $rootScope, $q, $view, $injector, $resolve, $stateParams, $location, $urlRouter) { - - var TransitionSuperseded = $q.reject(new Error('transition superseded')); - var TransitionPrevented = $q.reject(new Error('transition prevented')); - var TransitionAborted = $q.reject(new Error('transition aborted')); - var TransitionFailed = $q.reject(new Error('transition failed')); - var currentLocation = $location.url(); - - function syncUrl() { - if ($location.url() !== currentLocation) { - $location.url(currentLocation); - $location.replace(); + }; + StateQueueManager.prototype.flush = function () { + var _this = this; + var _a = this, queue = _a.queue, states = _a.states, builder = _a.builder; + var registered = [], // states that got registered + orphans = [], // states that don't yet have a parent registered + previousQueueLength = {}; // keep track of how long the queue when an orphan was first encountered + var getState = function (name) { + return _this.states.hasOwnProperty(name) && _this.states[name]; + }; + while (queue.length > 0) { + var state = queue.shift(); + var name_1 = state.name; + var result = builder.build(state); + var orphanIdx = orphans.indexOf(state); + if (result) { + var existingState = getState(name_1); + if (existingState && existingState.name === name_1) { + throw new Error("State '" + name_1 + "' is already defined"); + } + var existingFutureState = getState(name_1 + ".**"); + if (existingFutureState) { + // Remove future state of the same name + this.$registry.deregister(existingFutureState); + } + states[name_1] = state; + this.attachRoute(state); + if (orphanIdx >= 0) + orphans.splice(orphanIdx, 1); + registered.push(state); + continue; } + var prev = previousQueueLength[name_1]; + previousQueueLength[name_1] = queue.length; + if (orphanIdx >= 0 && prev === queue.length) { + // Wait until two consecutive iterations where no additional states were dequeued successfully. + // throw new Error(`Cannot register orphaned state '${name}'`); + queue.push(state); + return states; + } + else if (orphanIdx < 0) { + orphans.push(state); + } + queue.push(state); } + if (registered.length) { + this.listeners.forEach(function (listener) { return listener("registered", registered.map(function (s) { return s.self; })); }); + } + return states; + }; + StateQueueManager.prototype.attachRoute = function (state) { + if (state.abstract || !state.url) + return; + this.$urlRouter.rule(this.$urlRouter.urlRuleFactory.create(state)); + }; + return StateQueueManager; + }()); - root.locals = { resolve: null, globals: { $stateParams: {} } }; - $state = { + /** + * @coreapi + * @module state + */ /** for typedoc */ + var StateRegistry = /** @class */ (function () { + /** @internalapi */ + function StateRegistry(_router) { + this._router = _router; + this.states = {}; + this.listeners = []; + this.matcher = new StateMatcher(this.states); + this.builder = new StateBuilder(this.matcher, _router.urlMatcherFactory); + this.stateQueue = new StateQueueManager(this, _router.urlRouter, this.states, this.builder, this.listeners); + this._registerRoot(); + } + /** @internalapi */ + StateRegistry.prototype._registerRoot = function () { + var rootStateDef = { + name: '', + url: '^', + views: null, + params: { + '#': { value: null, type: 'hash', dynamic: true } + }, + abstract: true + }; + var _root = this._root = this.stateQueue.register(rootStateDef); + _root.navigable = null; + }; + /** @internalapi */ + StateRegistry.prototype.dispose = function () { + var _this = this; + this.stateQueue.dispose(); + this.listeners = []; + this.get().forEach(function (state) { return _this.get(state) && _this.deregister(state); }); + }; + /** + * Listen for a State Registry events + * + * Adds a callback that is invoked when states are registered or deregistered with the StateRegistry. + * + * #### Example: + * ```js + * let allStates = registry.get(); + * + * // Later, invoke deregisterFn() to remove the listener + * let deregisterFn = registry.onStatesChanged((event, states) => { + * switch(event) { + * case: 'registered': + * states.forEach(state => allStates.push(state)); + * break; + * case: 'deregistered': + * states.forEach(state => { + * let idx = allStates.indexOf(state); + * if (idx !== -1) allStates.splice(idx, 1); + * }); + * break; + * } + * }); + * ``` + * + * @param listener a callback function invoked when the registered states changes. + * The function receives two parameters, `event` and `state`. + * See [[StateRegistryListener]] + * @return a function that deregisters the listener + */ + StateRegistry.prototype.onStatesChanged = function (listener) { + this.listeners.push(listener); + return function deregisterListener() { + removeFrom(this.listeners)(listener); + }.bind(this); + }; + /** + * Gets the implicit root state + * + * Gets the root of the state tree. + * The root state is implicitly created by UI-Router. + * Note: this returns the internal [[StateObject]] representation, not a [[StateDeclaration]] + * + * @return the root [[StateObject]] + */ + StateRegistry.prototype.root = function () { + return this._root; + }; + /** + * Adds a state to the registry + * + * Registers a [[StateDeclaration]] or queues it for registration. + * + * Note: a state will be queued if the state's parent isn't yet registered. + * + * @param stateDefinition the definition of the state to register. + * @returns the internal [[StateObject]] object. + * If the state was successfully registered, then the object is fully built (See: [[StateBuilder]]). + * If the state was only queued, then the object is not fully built. + */ + StateRegistry.prototype.register = function (stateDefinition) { + return this.stateQueue.register(stateDefinition); + }; + /** @hidden */ + StateRegistry.prototype._deregisterTree = function (state) { + var _this = this; + var all$$1 = this.get().map(function (s) { return s.$$state(); }); + var getChildren = function (states) { + var children = all$$1.filter(function (s) { return states.indexOf(s.parent) !== -1; }); + return children.length === 0 ? children : children.concat(getChildren(children)); + }; + var children = getChildren([state]); + var deregistered = [state].concat(children).reverse(); + deregistered.forEach(function (state) { + var $ur = _this._router.urlRouter; + // Remove URL rule + $ur.rules().filter(propEq("state", state)).forEach($ur.removeRule.bind($ur)); + // Remove state from registry + delete _this.states[state.name]; + }); + return deregistered; + }; + /** + * Removes a state from the registry + * + * This removes a state from the registry. + * If the state has children, they are are also removed from the registry. + * + * @param stateOrName the state's name or object representation + * @returns {StateObject[]} a list of removed states + */ + StateRegistry.prototype.deregister = function (stateOrName) { + var _state = this.get(stateOrName); + if (!_state) + throw new Error("Can't deregister state; not found: " + stateOrName); + var deregisteredStates = this._deregisterTree(_state.$$state()); + this.listeners.forEach(function (listener) { return listener("deregistered", deregisteredStates.map(function (s) { return s.self; })); }); + return deregisteredStates; + }; + StateRegistry.prototype.get = function (stateOrName, base) { + var _this = this; + if (arguments.length === 0) + return Object.keys(this.states).map(function (name) { return _this.states[name].self; }); + var found = this.matcher.find(stateOrName, base); + return found && found.self || null; + }; + StateRegistry.prototype.decorator = function (name, func) { + return this.builder.builder(name, func); + }; + return StateRegistry; + }()); + + /** + * @coreapi + * @module url + */ + /** for typedoc */ + /** @hidden */ + function quoteRegExp(string, param) { + var surroundPattern = ['', ''], result = string.replace(/[\\\[\]\^$*+?.()|{}]/g, "\\$&"); + if (!param) + return result; + switch (param.squash) { + case false: + surroundPattern = ['(', ')' + (param.isOptional ? '?' : '')]; + break; + case true: + result = result.replace(/\/$/, ''); + surroundPattern = ['(?:\/(', ')|\/)?']; + break; + default: + surroundPattern = ["(" + param.squash + "|", ')?']; + break; + } + return result + surroundPattern[0] + param.type.pattern.source + surroundPattern[1]; + } + /** @hidden */ + var memoizeTo = function (obj, prop$$1, fn) { + return obj[prop$$1] = obj[prop$$1] || fn(); + }; + /** @hidden */ + var splitOnSlash = splitOnDelim('/'); + /** + * Matches URLs against patterns. + * + * Matches URLs against patterns and extracts named parameters from the path or the search + * part of the URL. + * + * A URL pattern consists of a path pattern, optionally followed by '?' and a list of search (query) + * parameters. Multiple search parameter names are separated by '&'. Search parameters + * do not influence whether or not a URL is matched, but their values are passed through into + * the matched parameters returned by [[UrlMatcher.exec]]. + * + * - *Path parameters* are defined using curly brace placeholders (`/somepath/{param}`) + * or colon placeholders (`/somePath/:param`). + * + * - *A parameter RegExp* may be defined for a param after a colon + * (`/somePath/{param:[a-zA-Z0-9]+}`) in a curly brace placeholder. + * The regexp must match for the url to be matched. + * Should the regexp itself contain curly braces, they must be in matched pairs or escaped with a backslash. + * + * Note: a RegExp parameter will encode its value using either [[ParamTypes.path]] or [[ParamTypes.query]]. + * + * - *Custom parameter types* may also be specified after a colon (`/somePath/{param:int}`) in curly brace parameters. + * See [[UrlMatcherFactory.type]] for more information. + * + * - *Catch-all parameters* are defined using an asterisk placeholder (`/somepath/*catchallparam`). + * A catch-all * parameter value will contain the remainder of the URL. + * + * --- + * + * Parameter names may contain only word characters (latin letters, digits, and underscore) and + * must be unique within the pattern (across both path and search parameters). + * A path parameter matches any number of characters other than '/'. For catch-all + * placeholders the path parameter matches any number of characters. + * + * Examples: + * + * * `'/hello/'` - Matches only if the path is exactly '/hello/'. There is no special treatment for + * trailing slashes, and patterns have to match the entire path, not just a prefix. + * * `'/user/:id'` - Matches '/user/bob' or '/user/1234!!!' or even '/user/' but not '/user' or + * '/user/bob/details'. The second path segment will be captured as the parameter 'id'. + * * `'/user/{id}'` - Same as the previous example, but using curly brace syntax. + * * `'/user/{id:[^/]*}'` - Same as the previous example. + * * `'/user/{id:[0-9a-fA-F]{1,8}}'` - Similar to the previous example, but only matches if the id + * parameter consists of 1 to 8 hex digits. + * * `'/files/{path:.*}'` - Matches any URL starting with '/files/' and captures the rest of the + * path into the parameter 'path'. + * * `'/files/*path'` - ditto. + * * `'/calendar/{start:date}'` - Matches "/calendar/2014-11-12" (because the pattern defined + * in the built-in `date` ParamType matches `2014-11-12`) and provides a Date object in $stateParams.start + * + */ + var UrlMatcher = /** @class */ (function () { + /** + * @param pattern The pattern to compile into a matcher. + * @param paramTypes The [[ParamTypes]] registry + * @param config A configuration object + * - `caseInsensitive` - `true` if URL matching should be case insensitive, otherwise `false`, the default value (for backward compatibility) is `false`. + * - `strict` - `false` if matching against a URL with a trailing slash should be treated as equivalent to a URL without a trailing slash, the default value is `true`. + */ + function UrlMatcher(pattern$$1, paramTypes, paramFactory, config) { + var _this = this; + this.config = config; + /** @hidden */ + this._cache = { path: [this] }; + /** @hidden */ + this._children = []; + /** @hidden */ + this._params = []; + /** @hidden */ + this._segments = []; + /** @hidden */ + this._compiled = []; + this.pattern = pattern$$1; + this.config = defaults(this.config, { params: {}, - current: root.self, - $current: root, - transition: null + strict: true, + caseInsensitive: false, + paramMap: identity + }); + // Find all placeholders and create a compiled pattern, using either classic or curly syntax: + // '*' name + // ':' name + // '{' name '}' + // '{' name ':' regexp '}' + // The regular expression is somewhat complicated due to the need to allow curly braces + // inside the regular expression. The placeholder regexp breaks down as follows: + // ([:*])([\w\[\]]+) - classic placeholder ($1 / $2) (search version has - for snake-case) + // \{([\w\[\]]+)(?:\:\s*( ... ))?\} - curly brace placeholder ($3) with optional regexp/type ... ($4) (search version has - for snake-case + // (?: ... | ... | ... )+ - the regexp consists of any number of atoms, an atom being either + // [^{}\\]+ - anything other than curly braces or backslash + // \\. - a backslash escape + // \{(?:[^{}\\]+|\\.)*\} - a matched set of curly braces containing other atoms + var placeholder = /([:*])([\w\[\]]+)|\{([\w\[\]]+)(?:\:\s*((?:[^{}\\]+|\\.|\{(?:[^{}\\]+|\\.)*\})+))?\}/g, searchPlaceholder = /([:]?)([\w\[\].-]+)|\{([\w\[\].-]+)(?:\:\s*((?:[^{}\\]+|\\.|\{(?:[^{}\\]+|\\.)*\})+))?\}/g, last = 0, m, patterns = []; + var checkParamErrors = function (id) { + if (!UrlMatcher.nameValidator.test(id)) + throw new Error("Invalid parameter name '" + id + "' in pattern '" + pattern$$1 + "'"); + if (find(_this._params, propEq('id', id))) + throw new Error("Duplicate parameter name '" + id + "' in pattern '" + pattern$$1 + "'"); }; - - $state.reload = function reload() { - $state.transitionTo($state.current, $stateParams, { reload: true, inherit: false, notify: false }); - }; - - $state.go = function go(to, params, options) { - return this.transitionTo(to, params, extend({ inherit: true, relative: $state.$current }, options)); - }; - - $state.transitionTo = function transitionTo(to, toParams, options) { - toParams = toParams || {}; - options = extend({ - location: true, inherit: false, relative: null, notify: true, reload: false, $retry: false - }, options || {}); - - var from = $state.$current, fromParams = $state.params, fromPath = from.path; - var evt, toState = findState(to, options.relative); - - if (!isDefined(toState)) { - // Broadcast not found event and abort the transition if prevented - var redirect = { to: to, toParams: toParams, options: options }; - evt = $rootScope.$broadcast('$stateNotFound', redirect, from.self, fromParams); - if (evt.defaultPrevented) { - syncUrl(); - return TransitionAborted; - } - - // Allow the handler to return a promise to defer state lookup retry - if (evt.retry) { - if (options.$retry) { - syncUrl(); - return TransitionFailed; - } - var retryTransition = $state.transition = $q.when(evt.retry); - retryTransition.then(function() { - if (retryTransition !== $state.transition) return TransitionSuperseded; - redirect.options.$retry = true; - return $state.transitionTo(redirect.to, redirect.toParams, redirect.options); - }, function() { - return TransitionAborted; - }); - syncUrl(); - return retryTransition; - } - - // Always retry once if the $stateNotFound was not prevented - // (handles either redirect changed or state lazy-definition) - to = redirect.to; - toParams = redirect.toParams; - options = redirect.options; - toState = findState(to, options.relative); - if (!isDefined(toState)) { - if (options.relative) throw new Error("Could not resolve '" + to + "' from state '" + options.relative + "'"); - throw new Error("No such state '" + to + "'"); - } - } - if (toState[abstractKey]) throw new Error("Cannot transition to abstract state '" + to + "'"); - if (options.inherit) toParams = inheritParams($stateParams, toParams || {}, $state.$current, toState); - to = toState; - - var toPath = to.path; - - // Starting from the root of the path, keep all levels that haven't changed - var keep, state, locals = root.locals, toLocals = []; - for (keep = 0, state = toPath[keep]; - state && state === fromPath[keep] && equalForKeys(toParams, fromParams, state.ownParams) && !options.reload; - keep++, state = toPath[keep]) { - locals = toLocals[keep] = state.locals; - } - - // If we're going to the same state and all locals are kept, we've got nothing to do. - // But clear 'transition', as we still want to cancel any other pending transitions. - // TODO: We may not want to bump 'transition' if we're called from a location change that we've initiated ourselves, - // because we might accidentally abort a legitimate transition initiated from code? - if (shouldTriggerReload(to, from, locals, options) ) { - if ( to.self.reloadOnSearch !== false ) - syncUrl(); - $state.transition = null; - return $q.when($state.current); - } - - // Normalize/filter parameters before we pass them to event handlers etc. - toParams = normalize(to.params, toParams || {}); - - // Broadcast start event and cancel the transition if requested - if (options.notify) { - evt = $rootScope.$broadcast('$stateChangeStart', to.self, toParams, from.self, fromParams); - if (evt.defaultPrevented) { - syncUrl(); - return TransitionPrevented; - } - } - - // Resolve locals for the remaining states, but don't update any global state just - // yet -- if anything fails to resolve the current state needs to remain untouched. - // We also set up an inheritance chain for the locals here. This allows the view directive - // to quickly look up the correct definition for each view in the current state. Even - // though we create the locals object itself outside resolveState(), it is initially - // empty and gets filled asynchronously. We need to keep track of the promise for the - // (fully resolved) current locals, and pass this down the chain. - var resolved = $q.when(locals); - for (var l=keep; l=keep; l--) { - exiting = fromPath[l]; - if (exiting.self.onExit) { - $injector.invoke(exiting.self.onExit, exiting.self, exiting.locals.globals); - } - exiting.locals = null; - } - - // Enter 'to' states not kept - for (l=keep; l').html(template).contents(); - animate.enter(contents, element); - return contents; - } - }, - "false": { - remove: function(element) { element.html(''); }, - restore: function(compiled, element) { element.append(compiled); }, - populate: function(template, element) { - element.html(template); - return element.contents(); - } - } - })[doAnimate.toString()]; - }; - - // Put back the compiled initial view - element.append(initialView); - - // Find the details of the parent view directive (if any) and use it - // to derive our own qualified view name, then hang our own details - // off the DOM so child directives can find it. - var parent = element.parent().inheritedData('$uiView'); - if (name.indexOf('@') < 0) name = name + '@' + (parent ? parent.state.name : ''); - var view = { name: name, state: null }; - element.data('$uiView', view); - - var eventHook = function() { - if (viewIsUpdating) return; - viewIsUpdating = true; - - try { updateView(true); } catch (e) { - viewIsUpdating = false; - throw e; - } - viewIsUpdating = false; - }; - - scope.$on('$stateChangeSuccess', eventHook); - scope.$on('$viewContentLoading', eventHook); - updateView(false); - - function updateView(doAnimate) { - var locals = $state.$current && $state.$current.locals[name]; - if (locals === viewLocals) return; // nothing to do - var render = renderer(animate && doAnimate); - - // Remove existing content - render.remove(element); - - // Destroy previous view scope - if (viewScope) { - viewScope.$destroy(); - viewScope = null; - } - - if (!locals) { - viewLocals = null; - view.state = null; - - // Restore the initial view - return render.restore(initialView, element); - } - - viewLocals = locals; - view.state = locals.$$state; - - var link = $compile(render.populate(locals.$template, element)); - viewScope = scope.$new(); - - if (locals.$$controller) { - locals.$scope = viewScope; - var controller = $controller(locals.$$controller, locals); - element.children().data('$ngControllerController', controller); - } - link(viewScope); - viewScope.$emit('$viewContentLoaded'); - if (onloadExp) viewScope.$eval(onloadExp); - - // TODO: This seems strange, shouldn't $anchorScroll listen for $viewContentLoaded if necessary? - // $anchorScroll might listen on event... - $anchorScroll(); - } + // Split into static segments separated by path parameter placeholders. + // The number of segments is always 1 more than the number of parameters. + var matchDetails = function (m, isSearch) { + // IE[78] returns '' for unmatched groups instead of null + var id = m[2] || m[3]; + var regexp = isSearch ? m[4] : m[4] || (m[1] === '*' ? '[\\s\\S]*' : null); + var makeRegexpType = function (regexp) { return inherit(paramTypes.type(isSearch ? "query" : "path"), { + pattern: new RegExp(regexp, _this.config.caseInsensitive ? 'i' : undefined) + }); }; + return { + id: id, + regexp: regexp, + cfg: _this.config.params[id], + segment: pattern$$1.substring(last, m.index), + type: !regexp ? null : paramTypes.type(regexp) || makeRegexpType(regexp) }; + }; + var p, segment; + while ((m = placeholder.exec(pattern$$1))) { + p = matchDetails(m, false); + if (p.segment.indexOf('?') >= 0) + break; // we're into the search part + checkParamErrors(p.id); + this._params.push(paramFactory.fromPath(p.id, p.type, this.config.paramMap(p.cfg, false))); + this._segments.push(p.segment); + patterns.push([p.segment, tail(this._params)]); + last = placeholder.lastIndex; } - }; - return directive; - } - - angular.module('ui.router.state').directive('uiView', $ViewDirective); - - function parseStateRef(ref) { - var parsed = ref.replace(/\n/g, " ").match(/^([^(]+?)\s*(\((.*)\))?$/); - if (!parsed || parsed.length !== 4) throw new Error("Invalid state ref '" + ref + "'"); - return { state: parsed[1], paramExpr: parsed[3] || null }; - } - - function stateContext(el) { - var stateData = el.parent().inheritedData('$uiView'); - - if (stateData && stateData.state && stateData.state.name) { - return stateData.state; + segment = pattern$$1.substring(last); + // Find any search parameter names and remove them from the last segment + var i = segment.indexOf('?'); + if (i >= 0) { + var search = segment.substring(i); + segment = segment.substring(0, i); + if (search.length > 0) { + last = 0; + while ((m = searchPlaceholder.exec(search))) { + p = matchDetails(m, true); + checkParamErrors(p.id); + this._params.push(paramFactory.fromSearch(p.id, p.type, this.config.paramMap(p.cfg, true))); + last = placeholder.lastIndex; + // check if ?& + } + } + } + this._segments.push(segment); + this._compiled = patterns.map(function (pattern$$1) { return quoteRegExp.apply(null, pattern$$1); }).concat(quoteRegExp(segment)); } - } - - $StateRefDirective.$inject = ['$state', '$timeout']; - function $StateRefDirective($state, $timeout) { - return { - restrict: 'A', - require: '?^uiSrefActive', - link: function(scope, element, attrs, uiSrefActive) { - var ref = parseStateRef(attrs.uiSref); - var params = null, url = null, base = stateContext(element) || $state.$current; - var isForm = element[0].nodeName === "FORM"; - var attr = isForm ? "action" : "href", nav = true; - - var update = function(newVal) { - if (newVal) params = newVal; - if (!nav) return; - - var newHref = $state.href(ref.state, params, { relative: base }); - - if (!newHref) { - nav = false; - return false; - } - element[0][attr] = newHref; - if (uiSrefActive) { - uiSrefActive.$$setStateInfo(ref.state, params); - } - }; - - if (ref.paramExpr) { - scope.$watch(ref.paramExpr, function(newVal, oldVal) { - if (newVal !== params) update(newVal); - }, true); - params = scope.$eval(ref.paramExpr); - } - update(); - - if (isForm) return; - - element.bind("click", function(e) { - var button = e.which || e.button; - - if ((button === 0 || button == 1) && !e.ctrlKey && !e.metaKey && !e.shiftKey) { - // HACK: This is to allow ng-clicks to be processed before the transition is initiated: - $timeout(function() { - scope.$apply(function() { - $state.go(ref.state, params, { relative: base }); - }); - }); - e.preventDefault(); - } - }); - } + /** + * Creates a new concatenated UrlMatcher + * + * Builds a new UrlMatcher by appending another UrlMatcher to this one. + * + * @param url A `UrlMatcher` instance to append as a child of the current `UrlMatcher`. + */ + UrlMatcher.prototype.append = function (url) { + this._children.push(url); + url._cache = { + path: this._cache.path.concat(url), + parent: this, + pattern: null, + }; + return url; }; - } - - $StateActiveDirective.$inject = ['$state', '$stateParams', '$interpolate']; - function $StateActiveDirective($state, $stateParams, $interpolate) { - return { - restrict: "A", - controller: function($scope, $element, $attrs) { - var state, params, activeClass; - - // There probably isn't much point in $observing this - activeClass = $interpolate($attrs.uiSrefActive || '', false)($scope); - - // Allow uiSref to communicate with uiSrefActive - this.$$setStateInfo = function(newState, newParams) { - state = $state.get(newState, stateContext($element)); - params = newParams; - update(); - }; - - $scope.$on('$stateChangeSuccess', update); - - // Update route state - function update() { - if ($state.$current.self === state && matchesParams()) { - $element.addClass(activeClass); - } else { - $element.removeClass(activeClass); - } - } - - function matchesParams() { - return !params || equalForKeys(params, $stateParams); - } - } + /** @hidden */ + UrlMatcher.prototype.isRoot = function () { + return this._cache.path[0] === this; }; - } - - angular.module('ui.router.state') - .directive('uiSref', $StateRefDirective) - .directive('uiSrefActive', $StateActiveDirective); - - $RouteProvider.$inject = ['$stateProvider', '$urlRouterProvider']; - function $RouteProvider( $stateProvider, $urlRouterProvider) { - - var routes = []; - - onEnterRoute.$inject = ['$$state']; - function onEnterRoute( $$state) { - /*jshint validthis: true */ - this.locals = $$state.locals.globals; - this.params = this.locals.$stateParams; - } - - function onExitRoute() { - /*jshint validthis: true */ - this.locals = null; - this.params = null; - } - - this.when = when; - function when(url, route) { - /*jshint validthis: true */ - if (route.redirectTo != null) { - // Redirect, configure directly on $urlRouterProvider - var redirect = route.redirectTo, handler; - if (isString(redirect)) { - handler = redirect; // leave $urlRouterProvider to handle - } else if (isFunction(redirect)) { - // Adapt to $urlRouterProvider API - handler = function (params, $location) { - return redirect(params, $location.path(), $location.search()); - }; - } else { - throw new Error("Invalid 'redirectTo' in when()"); - } - $urlRouterProvider.when(url, handler); - } else { - // Regular route, configure as state - $stateProvider.state(inherit(route, { - parent: null, - name: 'route:' + encodeURIComponent(url), - url: url, - onEnter: onEnterRoute, - onExit: onExitRoute - })); + /** Returns the input pattern string */ + UrlMatcher.prototype.toString = function () { + return this.pattern; + }; + /** + * Tests the specified url/path against this matcher. + * + * Tests if the given url matches this matcher's pattern, and returns an object containing the captured + * parameter values. Returns null if the path does not match. + * + * The returned object contains the values + * of any search parameters that are mentioned in the pattern, but their value may be null if + * they are not present in `search`. This means that search parameters are always treated + * as optional. + * + * #### Example: + * ```js + * new UrlMatcher('/user/{id}?q&r').exec('/user/bob', { + * x: '1', q: 'hello' + * }); + * // returns { id: 'bob', q: 'hello', r: null } + * ``` + * + * @param path The URL path to match, e.g. `$location.path()`. + * @param search URL search parameters, e.g. `$location.search()`. + * @param hash URL hash e.g. `$location.hash()`. + * @param options + * + * @returns The captured parameter values. + */ + UrlMatcher.prototype.exec = function (path, search, hash, options) { + var _this = this; + if (search === void 0) { search = {}; } + if (options === void 0) { options = {}; } + var match = memoizeTo(this._cache, 'pattern', function () { + return new RegExp([ + '^', + unnest(_this._cache.path.map(prop('_compiled'))).join(''), + _this.config.strict === false ? '\/?' : '', + '$' + ].join(''), _this.config.caseInsensitive ? 'i' : undefined); + }).exec(path); + if (!match) + return null; + //options = defaults(options, { isolate: false }); + var allParams = this.parameters(), pathParams = allParams.filter(function (param) { return !param.isSearch(); }), searchParams = allParams.filter(function (param) { return param.isSearch(); }), nPathSegments = this._cache.path.map(function (urlm) { return urlm._segments.length - 1; }).reduce(function (a, x) { return a + x; }), values$$1 = {}; + if (nPathSegments !== match.length - 1) + throw new Error("Unbalanced capture group in route '" + this.pattern + "'"); + function decodePathArray(string) { + var reverseString = function (str) { return str.split("").reverse().join(""); }; + var unquoteDashes = function (str) { return str.replace(/\\-/g, "-"); }; + var split = reverseString(string).split(/-(?!\\)/); + var allReversed = map(split, reverseString); + return map(allReversed, unquoteDashes).reverse(); } - routes.push(route); + for (var i = 0; i < nPathSegments; i++) { + var param = pathParams[i]; + var value = match[i + 1]; + // if the param value matches a pre-replace pair, replace the value before decoding. + for (var j = 0; j < param.replace.length; j++) { + if (param.replace[j].from === value) + value = param.replace[j].to; + } + if (value && param.array === true) + value = decodePathArray(value); + if (isDefined(value)) + value = param.type.decode(value); + values$$1[param.id] = param.value(value); + } + searchParams.forEach(function (param) { + var value = search[param.id]; + for (var j = 0; j < param.replace.length; j++) { + if (param.replace[j].from === value) + value = param.replace[j].to; + } + if (isDefined(value)) + value = param.type.decode(value); + values$$1[param.id] = param.value(value); + }); + if (hash) + values$$1["#"] = hash; + return values$$1; + }; + /** + * @hidden + * Returns all the [[Param]] objects of all path and search parameters of this pattern in order of appearance. + * + * @returns {Array.} An array of [[Param]] objects. Must be treated as read-only. If the + * pattern has no parameters, an empty array is returned. + */ + UrlMatcher.prototype.parameters = function (opts) { + if (opts === void 0) { opts = {}; } + if (opts.inherit === false) + return this._params; + return unnest(this._cache.path.map(function (matcher) { return matcher._params; })); + }; + /** + * @hidden + * Returns a single parameter from this UrlMatcher by id + * + * @param id + * @param opts + * @returns {T|Param|any|boolean|UrlMatcher|null} + */ + UrlMatcher.prototype.parameter = function (id, opts) { + var _this = this; + if (opts === void 0) { opts = {}; } + var findParam = function () { + for (var _i = 0, _a = _this._params; _i < _a.length; _i++) { + var param = _a[_i]; + if (param.id === id) + return param; + } + }; + var parent = this._cache.parent; + return findParam() || (opts.inherit !== false && parent && parent.parameter(id, opts)) || null; + }; + /** + * Validates the input parameter values against this UrlMatcher + * + * Checks an object hash of parameters to validate their correctness according to the parameter + * types of this `UrlMatcher`. + * + * @param params The object hash of parameters to validate. + * @returns Returns `true` if `params` validates, otherwise `false`. + */ + UrlMatcher.prototype.validates = function (params) { + var validParamVal = function (param, val$$1) { + return !param || param.validates(val$$1); + }; + params = params || {}; + // I'm not sure why this checks only the param keys passed in, and not all the params known to the matcher + var paramSchema = this.parameters().filter(function (paramDef) { return params.hasOwnProperty(paramDef.id); }); + return paramSchema.map(function (paramDef) { return validParamVal(paramDef, params[paramDef.id]); }).reduce(allTrueR, true); + }; + /** + * Given a set of parameter values, creates a URL from this UrlMatcher. + * + * Creates a URL that matches this pattern by substituting the specified values + * for the path and search parameters. + * + * #### Example: + * ```js + * new UrlMatcher('/user/{id}?q').format({ id:'bob', q:'yes' }); + * // returns '/user/bob?q=yes' + * ``` + * + * @param values the values to substitute for the parameters in this pattern. + * @returns the formatted URL (path and optionally search part). + */ + UrlMatcher.prototype.format = function (values$$1) { + if (values$$1 === void 0) { values$$1 = {}; } + // Build the full path of UrlMatchers (including all parent UrlMatchers) + var urlMatchers = this._cache.path; + // Extract all the static segments and Params (processed as ParamDetails) + // into an ordered array + var pathSegmentsAndParams = urlMatchers.map(UrlMatcher.pathSegmentsAndParams) + .reduce(unnestR, []) + .map(function (x) { return isString(x) ? x : getDetails(x); }); + // Extract the query params into a separate array + var queryParams = urlMatchers.map(UrlMatcher.queryParams) + .reduce(unnestR, []) + .map(getDetails); + var isInvalid = function (param) { return param.isValid === false; }; + if (pathSegmentsAndParams.concat(queryParams).filter(isInvalid).length) { + return null; + } + /** + * Given a Param, applies the parameter value, then returns detailed information about it + */ + function getDetails(param) { + // Normalize to typed value + var value = param.value(values$$1[param.id]); + var isValid = param.validates(value); + var isDefaultValue = param.isDefaultValue(value); + // Check if we're in squash mode for the parameter + var squash = isDefaultValue ? param.squash : false; + // Allow the Parameter's Type to encode the value + var encoded = param.type.encode(value); + return { param: param, value: value, isValid: isValid, isDefaultValue: isDefaultValue, squash: squash, encoded: encoded }; + } + // Build up the path-portion from the list of static segments and parameters + var pathString = pathSegmentsAndParams.reduce(function (acc, x) { + // The element is a static segment (a raw string); just append it + if (isString(x)) + return acc + x; + // Otherwise, it's a ParamDetails. + var squash = x.squash, encoded = x.encoded, param = x.param; + // If squash is === true, try to remove a slash from the path + if (squash === true) + return (acc.match(/\/$/)) ? acc.slice(0, -1) : acc; + // If squash is a string, use the string for the param value + if (isString(squash)) + return acc + squash; + if (squash !== false) + return acc; // ? + if (encoded == null) + return acc; + // If this parameter value is an array, encode the value using encodeDashes + if (isArray(encoded)) + return acc + map(encoded, UrlMatcher.encodeDashes).join("-"); + // If the parameter type is "raw", then do not encodeURIComponent + if (param.raw) + return acc + encoded; + // Encode the value + return acc + encodeURIComponent(encoded); + }, ""); + // Build the query string by applying parameter values (array or regular) + // then mapping to key=value, then flattening and joining using "&" + var queryString = queryParams.map(function (paramDetails) { + var param = paramDetails.param, squash = paramDetails.squash, encoded = paramDetails.encoded, isDefaultValue = paramDetails.isDefaultValue; + if (encoded == null || (isDefaultValue && squash !== false)) + return; + if (!isArray(encoded)) + encoded = [encoded]; + if (encoded.length === 0) + return; + if (!param.raw) + encoded = map(encoded, encodeURIComponent); + return encoded.map(function (val$$1) { return param.id + "=" + val$$1; }); + }).filter(identity).reduce(unnestR, []).join("&"); + // Concat the pathstring with the queryString (if exists) and the hashString (if exists) + return pathString + (queryString ? "?" + queryString : "") + (values$$1["#"] ? "#" + values$$1["#"] : ""); + }; + /** @hidden */ + UrlMatcher.encodeDashes = function (str) { + return encodeURIComponent(str).replace(/-/g, function (c) { return "%5C%" + c.charCodeAt(0).toString(16).toUpperCase(); }); + }; + /** @hidden Given a matcher, return an array with the matcher's path segments and path params, in order */ + UrlMatcher.pathSegmentsAndParams = function (matcher) { + var staticSegments = matcher._segments; + var pathParams = matcher._params.filter(function (p) { return p.location === exports.DefType.PATH; }); + return arrayTuples(staticSegments, pathParams.concat(undefined)) + .reduce(unnestR, []) + .filter(function (x) { return x !== "" && isDefined(x); }); + }; + /** @hidden Given a matcher, return an array with the matcher's query params */ + UrlMatcher.queryParams = function (matcher) { + return matcher._params.filter(function (p) { return p.location === exports.DefType.SEARCH; }); + }; + /** + * Compare two UrlMatchers + * + * This comparison function converts a UrlMatcher into static and dynamic path segments. + * Each static path segment is a static string between a path separator (slash character). + * Each dynamic segment is a path parameter. + * + * The comparison function sorts static segments before dynamic ones. + */ + UrlMatcher.compare = function (a, b) { + /** + * Turn a UrlMatcher and all its parent matchers into an array + * of slash literals '/', string literals, and Param objects + * + * This example matcher matches strings like "/foo/:param/tail": + * var matcher = $umf.compile("/foo").append($umf.compile("/:param")).append($umf.compile("/")).append($umf.compile("tail")); + * var result = segments(matcher); // [ '/', 'foo', '/', Param, '/', 'tail' ] + * + * Caches the result as `matcher._cache.segments` + */ + var segments = function (matcher) { + return matcher._cache.segments = matcher._cache.segments || + matcher._cache.path.map(UrlMatcher.pathSegmentsAndParams) + .reduce(unnestR, []) + .reduce(joinNeighborsR, []) + .map(function (x) { return isString(x) ? splitOnSlash(x) : x; }) + .reduce(unnestR, []); + }; + /** + * Gets the sort weight for each segment of a UrlMatcher + * + * Caches the result as `matcher._cache.weights` + */ + var weights = function (matcher) { + return matcher._cache.weights = matcher._cache.weights || + segments(matcher).map(function (segment) { + // Sort slashes first, then static strings, the Params + if (segment === '/') + return 1; + if (isString(segment)) + return 2; + if (segment instanceof Param) + return 3; + }); + }; + /** + * Pads shorter array in-place (mutates) + */ + var padArrays = function (l, r, padVal) { + var len = Math.max(l.length, r.length); + while (l.length < len) + l.push(padVal); + while (r.length < len) + r.push(padVal); + }; + var weightsA = weights(a), weightsB = weights(b); + padArrays(weightsA, weightsB, 0); + var cmp, i, pairs$$1 = arrayTuples(weightsA, weightsB); + for (i = 0; i < pairs$$1.length; i++) { + cmp = pairs$$1[i][0] - pairs$$1[i][1]; + if (cmp !== 0) + return cmp; + } + return 0; + }; + /** @hidden */ + UrlMatcher.nameValidator = /^\w+([-.]+\w+)*(?:\[\])?$/; + return UrlMatcher; + }()); + + /** + * @internalapi + * @module url + */ /** for typedoc */ + /** + * Factory for [[UrlMatcher]] instances. + * + * The factory is available to ng1 services as + * `$urlMatcherFactory` or ng1 providers as `$urlMatcherFactoryProvider`. + */ + var UrlMatcherFactory = /** @class */ (function () { + function UrlMatcherFactory() { + var _this = this; + /** @hidden */ this.paramTypes = new ParamTypes(); + /** @hidden */ this._isCaseInsensitive = false; + /** @hidden */ this._isStrictMode = true; + /** @hidden */ this._defaultSquashPolicy = false; + /** @hidden */ + this._getConfig = function (config) { + return extend({ strict: _this._isStrictMode, caseInsensitive: _this._isCaseInsensitive }, config); + }; + /** @internalapi Creates a new [[Param]] for a given location (DefType) */ + this.paramFactory = { + /** Creates a new [[Param]] from a CONFIG block */ + fromConfig: function (id, type, config) { + return new Param(id, type, config, exports.DefType.CONFIG, _this); + }, + /** Creates a new [[Param]] from a url PATH */ + fromPath: function (id, type, config) { + return new Param(id, type, config, exports.DefType.PATH, _this); + }, + /** Creates a new [[Param]] from a url SEARCH */ + fromSearch: function (id, type, config) { + return new Param(id, type, config, exports.DefType.SEARCH, _this); + }, + }; + extend(this, { UrlMatcher: UrlMatcher, Param: Param }); + } + /** @inheritdoc */ + UrlMatcherFactory.prototype.caseInsensitive = function (value) { + return this._isCaseInsensitive = isDefined(value) ? value : this._isCaseInsensitive; + }; + /** @inheritdoc */ + UrlMatcherFactory.prototype.strictMode = function (value) { + return this._isStrictMode = isDefined(value) ? value : this._isStrictMode; + }; + /** @inheritdoc */ + UrlMatcherFactory.prototype.defaultSquashPolicy = function (value) { + if (isDefined(value) && value !== true && value !== false && !isString(value)) + throw new Error("Invalid squash policy: " + value + ". Valid policies: false, true, arbitrary-string"); + return this._defaultSquashPolicy = isDefined(value) ? value : this._defaultSquashPolicy; + }; + /** + * Creates a [[UrlMatcher]] for the specified pattern. + * + * @param pattern The URL pattern. + * @param config The config object hash. + * @returns The UrlMatcher. + */ + UrlMatcherFactory.prototype.compile = function (pattern, config) { + return new UrlMatcher(pattern, this.paramTypes, this.paramFactory, this._getConfig(config)); + }; + /** + * Returns true if the specified object is a [[UrlMatcher]], or false otherwise. + * + * @param object The object to perform the type check against. + * @returns `true` if the object matches the `UrlMatcher` interface, by + * implementing all the same methods. + */ + UrlMatcherFactory.prototype.isMatcher = function (object) { + // TODO: typeof? + if (!isObject(object)) + return false; + var result = true; + forEach(UrlMatcher.prototype, function (val, name) { + if (isFunction(val)) + result = result && (isDefined(object[name]) && isFunction(object[name])); + }); + return result; + }; + + /** + * Creates and registers a custom [[ParamType]] object + * + * A [[ParamType]] can be used to generate URLs with typed parameters. + * + * @param name The type name. + * @param definition The type definition. See [[ParamTypeDefinition]] for information on the values accepted. + * @param definitionFn A function that is injected before the app runtime starts. + * The result of this function should be a [[ParamTypeDefinition]]. + * The result is merged into the existing `definition`. + * See [[ParamType]] for information on the values accepted. + * + * @returns - if a type was registered: the [[UrlMatcherFactory]] + * - if only the `name` parameter was specified: the currently registered [[ParamType]] object, or undefined + * + * Note: Register custom types *before using them* in a state definition. + * + * See [[ParamTypeDefinition]] for examples + */ + UrlMatcherFactory.prototype.type = function (name, definition, definitionFn) { + var type = this.paramTypes.type(name, definition, definitionFn); + return !isDefined(definition) ? type : this; + }; + + /** @hidden */ + UrlMatcherFactory.prototype.$get = function () { + this.paramTypes.enqueue = false; + this.paramTypes._flushTypeQueue(); return this; + }; + + /** @internalapi */ + UrlMatcherFactory.prototype.dispose = function () { + this.paramTypes.dispose(); + }; + return UrlMatcherFactory; + }()); + + /** + * @coreapi + * @module url + */ /** */ + /** + * Creates a [[UrlRule]] + * + * Creates a [[UrlRule]] from a: + * + * - `string` + * - [[UrlMatcher]] + * - `RegExp` + * - [[StateObject]] + * @internalapi + */ + var UrlRuleFactory = /** @class */ (function () { + function UrlRuleFactory(router) { + this.router = router; } - - this.$get = $get; - $get.$inject = ['$state', '$rootScope', '$routeParams']; - function $get( $state, $rootScope, $routeParams) { - - var $route = { - routes: routes, - params: $routeParams, - current: undefined - }; - - function stateAsRoute(state) { - return (state.name !== '') ? state : undefined; + UrlRuleFactory.prototype.compile = function (str) { + return this.router.urlMatcherFactory.compile(str); + }; + UrlRuleFactory.prototype.create = function (what, handler) { + var _this = this; + var makeRule = pattern([ + [isString, function (_what) { return makeRule(_this.compile(_what)); }], + [is(UrlMatcher), function (_what) { return _this.fromUrlMatcher(_what, handler); }], + [isState, function (_what) { return _this.fromState(_what, _this.router); }], + [is(RegExp), function (_what) { return _this.fromRegExp(_what, handler); }], + [isFunction, function (_what) { return new BaseUrlRule(_what, handler); }], + ]); + var rule = makeRule(what); + if (!rule) + throw new Error("invalid 'what' in when()"); + return rule; + }; + /** + * A UrlRule which matches based on a UrlMatcher + * + * The `handler` may be either a `string`, a [[UrlRuleHandlerFn]] or another [[UrlMatcher]] + * + * ## Handler as a function + * + * If `handler` is a function, the function is invoked with: + * + * - matched parameter values ([[RawParams]] from [[UrlMatcher.exec]]) + * - url: the current Url ([[UrlParts]]) + * - router: the router object ([[UIRouter]]) + * + * #### Example: + * ```js + * var urlMatcher = $umf.compile("/foo/:fooId/:barId"); + * var rule = factory.fromUrlMatcher(urlMatcher, match => "/home/" + match.fooId + "/" + match.barId); + * var match = rule.match('/foo/123/456'); // results in { fooId: '123', barId: '456' } + * var result = rule.handler(match); // '/home/123/456' + * ``` + * + * ## Handler as UrlMatcher + * + * If `handler` is a UrlMatcher, the handler matcher is used to create the new url. + * The `handler` UrlMatcher is formatted using the matched param from the first matcher. + * The url is replaced with the result. + * + * #### Example: + * ```js + * var urlMatcher = $umf.compile("/foo/:fooId/:barId"); + * var handler = $umf.compile("/home/:fooId/:barId"); + * var rule = factory.fromUrlMatcher(urlMatcher, handler); + * var match = rule.match('/foo/123/456'); // results in { fooId: '123', barId: '456' } + * var result = rule.handler(match); // '/home/123/456' + * ``` + */ + UrlRuleFactory.prototype.fromUrlMatcher = function (urlMatcher, handler) { + var _handler = handler; + if (isString(handler)) + handler = this.router.urlMatcherFactory.compile(handler); + if (is(UrlMatcher)(handler)) + _handler = function (match) { return handler.format(match); }; + function match(url) { + var match = urlMatcher.exec(url.path, url.search, url.hash); + return urlMatcher.validates(match) && match; } - - $rootScope.$on('$stateChangeStart', function (ev, to, toParams, from, fromParams) { - $rootScope.$broadcast('$routeChangeStart', stateAsRoute(to), stateAsRoute(from)); - }); - - $rootScope.$on('$stateChangeSuccess', function (ev, to, toParams, from, fromParams) { - $route.current = stateAsRoute(to); - $rootScope.$broadcast('$routeChangeSuccess', stateAsRoute(to), stateAsRoute(from)); - copy(toParams, $route.params); - }); - - $rootScope.$on('$stateChangeError', function (ev, to, toParams, from, fromParams, error) { - $rootScope.$broadcast('$routeChangeError', stateAsRoute(to), stateAsRoute(from), error); - }); - - return $route; + // Prioritize URLs, lowest to highest: + // - Some optional URL parameters, but none matched + // - No optional parameters in URL + // - Some optional parameters, some matched + // - Some optional parameters, all matched + function matchPriority(params) { + var optional = urlMatcher.parameters().filter(function (param) { return param.isOptional; }); + if (!optional.length) + return 0.000001; + var matched = optional.filter(function (param) { return params[param.id]; }); + return matched.length / optional.length; + } + var details = { urlMatcher: urlMatcher, matchPriority: matchPriority, type: "URLMATCHER" }; + return extend(new BaseUrlRule(match, _handler), details); + }; + /** + * A UrlRule which matches a state by its url + * + * #### Example: + * ```js + * var rule = factory.fromState($state.get('foo'), router); + * var match = rule.match('/foo/123/456'); // results in { fooId: '123', barId: '456' } + * var result = rule.handler(match); + * // Starts a transition to 'foo' with params: { fooId: '123', barId: '456' } + * ``` + */ + UrlRuleFactory.prototype.fromState = function (state, router) { + /** + * Handles match by transitioning to matched state + * + * First checks if the router should start a new transition. + * A new transition is not required if the current state's URL + * and the new URL are already identical + */ + var handler = function (match) { + var $state = router.stateService; + var globals = router.globals; + if ($state.href(state, match) !== $state.href(globals.current, globals.params)) { + $state.transitionTo(state, match, { inherit: true, source: "url" }); + } + }; + var details = { state: state, type: "STATE" }; + return extend(this.fromUrlMatcher(state.url, handler), details); + }; + /** + * A UrlRule which matches based on a regular expression + * + * The `handler` may be either a [[UrlRuleHandlerFn]] or a string. + * + * ## Handler as a function + * + * If `handler` is a function, the function is invoked with: + * + * - regexp match array (from `regexp`) + * - url: the current Url ([[UrlParts]]) + * - router: the router object ([[UIRouter]]) + * + * #### Example: + * ```js + * var rule = factory.fromRegExp(/^\/foo\/(bar|baz)$/, match => "/home/" + match[1]) + * var match = rule.match('/foo/bar'); // results in [ '/foo/bar', 'bar' ] + * var result = rule.handler(match); // '/home/bar' + * ``` + * + * ## Handler as string + * + * If `handler` is a string, the url is *replaced by the string* when the Rule is invoked. + * The string is first interpolated using `string.replace()` style pattern. + * + * #### Example: + * ```js + * var rule = factory.fromRegExp(/^\/foo\/(bar|baz)$/, "/home/$1") + * var match = rule.match('/foo/bar'); // results in [ '/foo/bar', 'bar' ] + * var result = rule.handler(match); // '/home/bar' + * ``` + */ + UrlRuleFactory.prototype.fromRegExp = function (regexp, handler) { + if (regexp.global || regexp.sticky) + throw new Error("Rule RegExp must not be global or sticky"); + /** + * If handler is a string, the url will be replaced by the string. + * If the string has any String.replace() style variables in it (like `$2`), + * they will be replaced by the captures from [[match]] + */ + var redirectUrlTo = function (match) { + // Interpolates matched values into $1 $2, etc using a String.replace()-style pattern + return handler.replace(/\$(\$|\d{1,2})/, function (m, what) { + return match[what === '$' ? 0 : Number(what)]; + }); + }; + var _handler = isString(handler) ? redirectUrlTo : handler; + var match = function (url) { + return regexp.exec(url.path); + }; + var details = { regexp: regexp, type: "REGEXP" }; + return extend(new BaseUrlRule(match, _handler), details); + }; + UrlRuleFactory.isUrlRule = function (obj) { + return obj && ['type', 'match', 'handler'].every(function (key) { return isDefined(obj[key]); }); + }; + return UrlRuleFactory; + }()); + /** + * A base rule which calls `match` + * + * The value from the `match` function is passed through to the `handler`. + * @internalapi + */ + var BaseUrlRule = /** @class */ (function () { + function BaseUrlRule(match, handler) { + var _this = this; + this.match = match; + this.type = "RAW"; + this.matchPriority = function (match) { return 0 - _this.$id; }; + this.handler = handler || identity; } + return BaseUrlRule; + }()); + + /** + * @internalapi + * @module url + */ + /** for typedoc */ + /** @hidden */ + function appendBasePath(url, isHtml5, absolute, baseHref) { + if (baseHref === '/') + return url; + if (isHtml5) + return stripFile(baseHref) + url; + if (absolute) + return baseHref.slice(1) + url; + return url; + } + /** @hidden */ + var prioritySort = function (a, b) { + return (b.priority || 0) - (a.priority || 0); + }; + /** @hidden */ + var typeSort = function (a, b) { + var weights = { "STATE": 4, "URLMATCHER": 4, "REGEXP": 3, "RAW": 2, "OTHER": 1 }; + return (weights[a.type] || 0) - (weights[b.type] || 0); + }; + /** @hidden */ + var urlMatcherSort = function (a, b) { + return !a.urlMatcher || !b.urlMatcher ? 0 : UrlMatcher.compare(a.urlMatcher, b.urlMatcher); + }; + /** @hidden */ + var idSort = function (a, b) { + // Identically sorted STATE and URLMATCHER best rule will be chosen by `matchPriority` after each rule matches the URL + var useMatchPriority = { STATE: true, URLMATCHER: true }; + var equal = useMatchPriority[a.type] && useMatchPriority[b.type]; + return equal ? 0 : (a.$id || 0) - (b.$id || 0); + }; + /** + * Default rule priority sorting function. + * + * Sorts rules by: + * + * - Explicit priority (set rule priority using [[UrlRulesApi.when]]) + * - Rule type (STATE: 4, URLMATCHER: 4, REGEXP: 3, RAW: 2, OTHER: 1) + * - `UrlMatcher` specificity ([[UrlMatcher.compare]]): works for STATE and URLMATCHER types to pick the most specific rule. + * - Rule registration order (for rule types other than STATE and URLMATCHER) + * - Equally sorted State and UrlMatcher rules will each match the URL. + * Then, the *best* match is chosen based on how many parameter values were matched. + * + * @coreapi + */ + var defaultRuleSortFn; + defaultRuleSortFn = function (a, b) { + var cmp = prioritySort(a, b); + if (cmp !== 0) + return cmp; + cmp = typeSort(a, b); + if (cmp !== 0) + return cmp; + cmp = urlMatcherSort(a, b); + if (cmp !== 0) + return cmp; + return idSort(a, b); + }; + /** + * Updates URL and responds to URL changes + * + * ### Deprecation warning: + * This class is now considered to be an internal API + * Use the [[UrlService]] instead. + * For configuring URL rules, use the [[UrlRulesApi]] which can be found as [[UrlService.rules]]. + * + * This class updates the URL when the state changes. + * It also responds to changes in the URL. + */ + var UrlRouter = /** @class */ (function () { + /** @hidden */ + function UrlRouter(router) { + /** @hidden */ this._sortFn = defaultRuleSortFn; + /** @hidden */ this._rules = []; + /** @hidden */ this.interceptDeferred = false; + /** @hidden */ this._id = 0; + /** @hidden */ this._sorted = false; + this._router = router; + this.urlRuleFactory = new UrlRuleFactory(router); + createProxyFunctions(val(UrlRouter.prototype), this, val(this)); + } + /** @internalapi */ + UrlRouter.prototype.dispose = function () { + this.listen(false); + this._rules = []; + delete this._otherwiseFn; + }; + /** @inheritdoc */ + UrlRouter.prototype.sort = function (compareFn) { + this._rules = this.stableSort(this._rules, this._sortFn = compareFn || this._sortFn); + this._sorted = true; + }; + UrlRouter.prototype.ensureSorted = function () { + this._sorted || this.sort(); + }; + UrlRouter.prototype.stableSort = function (arr, compareFn) { + var arrOfWrapper = arr.map(function (elem, idx) { return ({ elem: elem, idx: idx }); }); + arrOfWrapper.sort(function (wrapperA, wrapperB) { + var cmpDiff = compareFn(wrapperA.elem, wrapperB.elem); + return cmpDiff === 0 + ? wrapperA.idx - wrapperB.idx + : cmpDiff; + }); + return arrOfWrapper.map(function (wrapper) { return wrapper.elem; }); + }; + /** + * Given a URL, check all rules and return the best [[MatchResult]] + * @param url + * @returns {MatchResult} + */ + UrlRouter.prototype.match = function (url) { + var _this = this; + this.ensureSorted(); + url = extend({ path: '', search: {}, hash: '' }, url); + var rules = this.rules(); + if (this._otherwiseFn) + rules.push(this._otherwiseFn); + // Checks a single rule. Returns { rule: rule, match: match, weight: weight } if it matched, or undefined + var checkRule = function (rule) { + var match = rule.match(url, _this._router); + return match && { match: match, rule: rule, weight: rule.matchPriority(match) }; + }; + // The rules are pre-sorted. + // - Find the first matching rule. + // - Find any other matching rule that sorted *exactly the same*, according to `.sort()`. + // - Choose the rule with the highest match weight. + var best; + for (var i = 0; i < rules.length; i++) { + // Stop when there is a 'best' rule and the next rule sorts differently than it. + if (best && this._sortFn(rules[i], best.rule) !== 0) + break; + var current = checkRule(rules[i]); + // Pick the best MatchResult + best = (!best || current && current.weight > best.weight) ? current : best; + } + return best; + }; + /** @inheritdoc */ + UrlRouter.prototype.sync = function (evt) { + if (evt && evt.defaultPrevented) + return; + var router = this._router, $url = router.urlService, $state = router.stateService; + var url = { + path: $url.path(), search: $url.search(), hash: $url.hash(), + }; + var best = this.match(url); + var applyResult = pattern([ + [isString, function (newurl) { return $url.url(newurl, true); }], + [TargetState.isDef, function (def) { return $state.go(def.state, def.params, def.options); }], + [is(TargetState), function (target) { return $state.go(target.state(), target.params(), target.options()); }], + ]); + applyResult(best && best.rule.handler(best.match, url, router)); + }; + /** @inheritdoc */ + UrlRouter.prototype.listen = function (enabled) { + var _this = this; + if (enabled === false) { + this._stopFn && this._stopFn(); + delete this._stopFn; + } + else { + return this._stopFn = this._stopFn || this._router.urlService.onChange(function (evt) { return _this.sync(evt); }); + } + }; + /** + * Internal API. + * @internalapi + */ + UrlRouter.prototype.update = function (read) { + var $url = this._router.locationService; + if (read) { + this.location = $url.path(); + return; + } + if ($url.path() === this.location) + return; + $url.url(this.location, true); + }; + /** + * Internal API. + * + * Pushes a new location to the browser history. + * + * @internalapi + * @param urlMatcher + * @param params + * @param options + */ + UrlRouter.prototype.push = function (urlMatcher, params, options) { + var replace = options && !!options.replace; + this._router.urlService.url(urlMatcher.format(params || {}), replace); + }; + /** + * Builds and returns a URL with interpolated parameters + * + * #### Example: + * ```js + * matcher = $umf.compile("/about/:person"); + * params = { person: "bob" }; + * $bob = $urlRouter.href(matcher, params); + * // $bob == "/about/bob"; + * ``` + * + * @param urlMatcher The [[UrlMatcher]] object which is used as the template of the URL to generate. + * @param params An object of parameter values to fill the matcher's required parameters. + * @param options Options object. The options are: + * + * - **`absolute`** - {boolean=false}, If true will generate an absolute url, e.g. "http://www.example.com/fullurl". + * + * @returns Returns the fully compiled URL, or `null` if `params` fail validation against `urlMatcher` + */ + UrlRouter.prototype.href = function (urlMatcher, params, options) { + var url = urlMatcher.format(params); + if (url == null) + return null; + options = options || { absolute: false }; + var cfg = this._router.urlService.config; + var isHtml5 = cfg.html5Mode(); + if (!isHtml5 && url !== null) { + url = "#" + cfg.hashPrefix() + url; + } + url = appendBasePath(url, isHtml5, options.absolute, cfg.baseHref()); + if (!options.absolute || !url) { + return url; + } + var slash = (!isHtml5 && url ? '/' : ''), port = cfg.port(); + port = (port === 80 || port === 443 ? '' : ':' + port); + return [cfg.protocol(), '://', cfg.host(), port, slash, url].join(''); + }; + /** + * Manually adds a URL Rule. + * + * Usually, a url rule is added using [[StateDeclaration.url]] or [[when]]. + * This api can be used directly for more control (to register a [[BaseUrlRule]], for example). + * Rules can be created using [[UrlRouter.urlRuleFactory]], or create manually as simple objects. + * + * A rule should have a `match` function which returns truthy if the rule matched. + * It should also have a `handler` function which is invoked if the rule is the best match. + * + * @return a function that deregisters the rule + */ + UrlRouter.prototype.rule = function (rule) { + var _this = this; + if (!UrlRuleFactory.isUrlRule(rule)) + throw new Error("invalid rule"); + rule.$id = this._id++; + rule.priority = rule.priority || 0; + this._rules.push(rule); + this._sorted = false; + return function () { return _this.removeRule(rule); }; + }; + /** @inheritdoc */ + UrlRouter.prototype.removeRule = function (rule) { + removeFrom(this._rules, rule); + }; + /** @inheritdoc */ + UrlRouter.prototype.rules = function () { + this.ensureSorted(); + return this._rules.slice(); + }; + /** @inheritdoc */ + UrlRouter.prototype.otherwise = function (handler) { + var handlerFn = getHandlerFn(handler); + this._otherwiseFn = this.urlRuleFactory.create(val(true), handlerFn); + this._sorted = false; + }; + + /** @inheritdoc */ + UrlRouter.prototype.initial = function (handler) { + var handlerFn = getHandlerFn(handler); + var matchFn = function (urlParts, router) { + return router.globals.transitionHistory.size() === 0 && !!/^\/?$/.exec(urlParts.path); + }; + this.rule(this.urlRuleFactory.create(matchFn, handlerFn)); + }; + + /** @inheritdoc */ + UrlRouter.prototype.when = function (matcher, handler, options) { + var rule = this.urlRuleFactory.create(matcher, handler); + if (isDefined(options && options.priority)) + rule.priority = options.priority; + this.rule(rule); + return rule; + }; + + /** @inheritdoc */ + UrlRouter.prototype.deferIntercept = function (defer) { + if (defer === undefined) + defer = true; + this.interceptDeferred = defer; + }; + + return UrlRouter; + }()); + function getHandlerFn(handler) { + if (!isFunction(handler) && !isString(handler) && !is(TargetState)(handler) && !TargetState.isDef(handler)) { + throw new Error("'handler' must be a string, function, TargetState, or have a state: 'newtarget' property"); + } + return isFunction(handler) ? handler : val(handler); } - angular.module('ui.router.compat') - .provider('$route', $RouteProvider) - .directive('ngView', $ViewDirective); -})(window, window.angular); \ No newline at end of file + /** + * @coreapi + * @module view + */ /** for typedoc */ + /** + * The View service + * + * This service pairs existing `ui-view` components (which live in the DOM) + * with view configs (from the state declaration objects: [[StateDeclaration.views]]). + * + * - After a successful Transition, the views from the newly entered states are activated via [[activateViewConfig]]. + * The views from exited states are deactivated via [[deactivateViewConfig]]. + * (See: the [[registerActivateViews]] Transition Hook) + * + * - As `ui-view` components pop in and out of existence, they register themselves using [[registerUIView]]. + * + * - When the [[sync]] function is called, the registered `ui-view`(s) ([[ActiveUIView]]) + * are configured with the matching [[ViewConfig]](s) + * + */ + var ViewService = /** @class */ (function () { + function ViewService() { + var _this = this; + this._uiViews = []; + this._viewConfigs = []; + this._viewConfigFactories = {}; + this._pluginapi = { + _rootViewContext: this._rootViewContext.bind(this), + _viewConfigFactory: this._viewConfigFactory.bind(this), + _registeredUIViews: function () { return _this._uiViews; }, + _activeViewConfigs: function () { return _this._viewConfigs; }, + }; + } + ViewService.prototype._rootViewContext = function (context) { + return this._rootContext = context || this._rootContext; + }; + + ViewService.prototype._viewConfigFactory = function (viewType, factory) { + this._viewConfigFactories[viewType] = factory; + }; + ViewService.prototype.createViewConfig = function (path, decl) { + var cfgFactory = this._viewConfigFactories[decl.$type]; + if (!cfgFactory) + throw new Error("ViewService: No view config factory registered for type " + decl.$type); + var cfgs = cfgFactory(path, decl); + return isArray(cfgs) ? cfgs : [cfgs]; + }; + /** + * Deactivates a ViewConfig. + * + * This function deactivates a `ViewConfig`. + * After calling [[sync]], it will un-pair from any `ui-view` with which it is currently paired. + * + * @param viewConfig The ViewConfig view to deregister. + */ + ViewService.prototype.deactivateViewConfig = function (viewConfig) { + trace.traceViewServiceEvent("<- Removing", viewConfig); + removeFrom(this._viewConfigs, viewConfig); + }; + ViewService.prototype.activateViewConfig = function (viewConfig) { + trace.traceViewServiceEvent("-> Registering", viewConfig); + this._viewConfigs.push(viewConfig); + }; + ViewService.prototype.sync = function () { + var _this = this; + var uiViewsByFqn = this._uiViews.map(function (uiv) { return [uiv.fqn, uiv]; }).reduce(applyPairs, {}); + // Return a weighted depth value for a uiView. + // The depth is the nesting depth of ui-views (based on FQN; times 10,000) + // plus the depth of the state that is populating the uiView + function uiViewDepth(uiView) { + var stateDepth = function (context) { + return context && context.parent ? stateDepth(context.parent) + 1 : 1; + }; + return (uiView.fqn.split(".").length * 10000) + stateDepth(uiView.creationContext); + } + // Return the ViewConfig's context's depth in the context tree. + function viewConfigDepth(config) { + var context = config.viewDecl.$context, count = 0; + while (++count && context.parent) + context = context.parent; + return count; + } + // Given a depth function, returns a compare function which can return either ascending or descending order + var depthCompare = curry(function (depthFn, posNeg, left, right) { return posNeg * (depthFn(left) - depthFn(right)); }); + var matchingConfigPair = function (uiView) { + var matchingConfigs = _this._viewConfigs.filter(ViewService.matches(uiViewsByFqn, uiView)); + if (matchingConfigs.length > 1) { + // This is OK. Child states can target a ui-view that the parent state also targets (the child wins) + // Sort by depth and return the match from the deepest child + // console.log(`Multiple matching view configs for ${uiView.fqn}`, matchingConfigs); + matchingConfigs.sort(depthCompare(viewConfigDepth, -1)); // descending + } + return [uiView, matchingConfigs[0]]; + }; + var configureUIView = function (_a) { + var uiView = _a[0], viewConfig = _a[1]; + // If a parent ui-view is reconfigured, it could destroy child ui-views. + // Before configuring a child ui-view, make sure it's still in the active uiViews array. + if (_this._uiViews.indexOf(uiView) !== -1) + uiView.configUpdated(viewConfig); + }; + // Sort views by FQN and state depth. Process uiviews nearest the root first. + var pairs$$1 = this._uiViews.sort(depthCompare(uiViewDepth, 1)).map(matchingConfigPair); + trace.traceViewSync(pairs$$1); + pairs$$1.forEach(configureUIView); + }; + + /** + * Registers a `ui-view` component + * + * When a `ui-view` component is created, it uses this method to register itself. + * After registration the [[sync]] method is used to ensure all `ui-view` are configured with the proper [[ViewConfig]]. + * + * Note: the `ui-view` component uses the `ViewConfig` to determine what view should be loaded inside the `ui-view`, + * and what the view's state context is. + * + * Note: There is no corresponding `deregisterUIView`. + * A `ui-view` should hang on to the return value of `registerUIView` and invoke it to deregister itself. + * + * @param uiView The metadata for a UIView + * @return a de-registration function used when the view is destroyed. + */ + ViewService.prototype.registerUIView = function (uiView) { + trace.traceViewServiceUIViewEvent("-> Registering", uiView); + var uiViews = this._uiViews; + var fqnAndTypeMatches = function (uiv) { return uiv.fqn === uiView.fqn && uiv.$type === uiView.$type; }; + if (uiViews.filter(fqnAndTypeMatches).length) + trace.traceViewServiceUIViewEvent("!!!! duplicate uiView named:", uiView); + uiViews.push(uiView); + this.sync(); + return function () { + var idx = uiViews.indexOf(uiView); + if (idx === -1) { + trace.traceViewServiceUIViewEvent("Tried removing non-registered uiView", uiView); + return; + } + trace.traceViewServiceUIViewEvent("<- Deregistering", uiView); + removeFrom(uiViews)(uiView); + }; + }; + + /** + * Returns the list of views currently available on the page, by fully-qualified name. + * + * @return {Array} Returns an array of fully-qualified view names. + */ + ViewService.prototype.available = function () { + return this._uiViews.map(prop("fqn")); + }; + /** + * Returns the list of views on the page containing loaded content. + * + * @return {Array} Returns an array of fully-qualified view names. + */ + ViewService.prototype.active = function () { + return this._uiViews.filter(prop("$config")).map(prop("name")); + }; + /** + * Normalizes a view's name from a state.views configuration block. + * + * This should be used by a framework implementation to calculate the values for + * [[_ViewDeclaration.$uiViewName]] and [[_ViewDeclaration.$uiViewContextAnchor]]. + * + * @param context the context object (state declaration) that the view belongs to + * @param rawViewName the name of the view, as declared in the [[StateDeclaration.views]] + * + * @returns the normalized uiViewName and uiViewContextAnchor that the view targets + */ + ViewService.normalizeUIViewTarget = function (context, rawViewName) { + if (rawViewName === void 0) { rawViewName = ""; } + // TODO: Validate incoming view name with a regexp to allow: + // ex: "view.name@foo.bar" , "^.^.view.name" , "view.name@^.^" , "" , + // "@" , "$default@^" , "!$default.$default" , "!foo.bar" + var viewAtContext = rawViewName.split("@"); + var uiViewName = viewAtContext[0] || "$default"; // default to unnamed view + var uiViewContextAnchor = isString(viewAtContext[1]) ? viewAtContext[1] : "^"; // default to parent context + // Handle relative view-name sugar syntax. + // Matches rawViewName "^.^.^.foo.bar" into array: ["^.^.^.foo.bar", "^.^.^", "foo.bar"], + var relativeViewNameSugar = /^(\^(?:\.\^)*)\.(.*$)/.exec(uiViewName); + if (relativeViewNameSugar) { + // Clobbers existing contextAnchor (rawViewName validation will fix this) + uiViewContextAnchor = relativeViewNameSugar[1]; // set anchor to "^.^.^" + uiViewName = relativeViewNameSugar[2]; // set view-name to "foo.bar" + } + if (uiViewName.charAt(0) === '!') { + uiViewName = uiViewName.substr(1); + uiViewContextAnchor = ""; // target absolutely from root + } + // handle parent relative targeting "^.^.^" + var relativeMatch = /^(\^(?:\.\^)*)$/; + if (relativeMatch.exec(uiViewContextAnchor)) { + var anchor = uiViewContextAnchor.split(".").reduce((function (anchor, x) { return anchor.parent; }), context); + uiViewContextAnchor = anchor.name; + } + else if (uiViewContextAnchor === '.') { + uiViewContextAnchor = context.name; + } + return { uiViewName: uiViewName, uiViewContextAnchor: uiViewContextAnchor }; + }; + /** + * Given a ui-view and a ViewConfig, determines if they "match". + * + * A ui-view has a fully qualified name (fqn) and a context object. The fqn is built from its overall location in + * the DOM, describing its nesting relationship to any parent ui-view tags it is nested inside of. + * + * A ViewConfig has a target ui-view name and a context anchor. The ui-view name can be a simple name, or + * can be a segmented ui-view path, describing a portion of a ui-view fqn. + * + * In order for a ui-view to match ViewConfig, ui-view's $type must match the ViewConfig's $type + * + * If the ViewConfig's target ui-view name is a simple name (no dots), then a ui-view matches if: + * - the ui-view's name matches the ViewConfig's target name + * - the ui-view's context matches the ViewConfig's anchor + * + * If the ViewConfig's target ui-view name is a segmented name (with dots), then a ui-view matches if: + * - There exists a parent ui-view where: + * - the parent ui-view's name matches the first segment (index 0) of the ViewConfig's target name + * - the parent ui-view's context matches the ViewConfig's anchor + * - And the remaining segments (index 1..n) of the ViewConfig's target name match the tail of the ui-view's fqn + * + * Example: + * + * DOM: + * + * + * + * + * + * + * + * + * + * uiViews: [ + * { fqn: "$default", creationContext: { name: "" } }, + * { fqn: "$default.foo", creationContext: { name: "A" } }, + * { fqn: "$default.foo.$default", creationContext: { name: "A.B" } } + * { fqn: "$default.foo.$default.bar", creationContext: { name: "A.B.C" } } + * ] + * + * These four view configs all match the ui-view with the fqn: "$default.foo.$default.bar": + * + * - ViewConfig1: { uiViewName: "bar", uiViewContextAnchor: "A.B.C" } + * - ViewConfig2: { uiViewName: "$default.bar", uiViewContextAnchor: "A.B" } + * - ViewConfig3: { uiViewName: "foo.$default.bar", uiViewContextAnchor: "A" } + * - ViewConfig4: { uiViewName: "$default.foo.$default.bar", uiViewContextAnchor: "" } + * + * Using ViewConfig3 as an example, it matches the ui-view with fqn "$default.foo.$default.bar" because: + * - The ViewConfig's segmented target name is: [ "foo", "$default", "bar" ] + * - There exists a parent ui-view (which has fqn: "$default.foo") where: + * - the parent ui-view's name "foo" matches the first segment "foo" of the ViewConfig's target name + * - the parent ui-view's context "A" matches the ViewConfig's anchor context "A" + * - And the remaining segments [ "$default", "bar" ].join("."_ of the ViewConfig's target name match + * the tail of the ui-view's fqn "default.bar" + * + * @internalapi + */ + ViewService.matches = function (uiViewsByFqn, uiView) { return function (viewConfig) { + // Don't supply an ng1 ui-view with an ng2 ViewConfig, etc + if (uiView.$type !== viewConfig.viewDecl.$type) + return false; + // Split names apart from both viewConfig and uiView into segments + var vc = viewConfig.viewDecl; + var vcSegments = vc.$uiViewName.split("."); + var uivSegments = uiView.fqn.split("."); + // Check if the tails of the segment arrays match. ex, these arrays' tails match: + // vc: ["foo", "bar"], uiv fqn: ["$default", "foo", "bar"] + if (!equals(vcSegments, uivSegments.slice(0 - vcSegments.length))) + return false; + // Now check if the fqn ending at the first segment of the viewConfig matches the context: + // ["$default", "foo"].join(".") == "$default.foo", does the ui-view $default.foo context match? + var negOffset = (1 - vcSegments.length) || undefined; + var fqnToFirstSegment = uivSegments.slice(0, negOffset).join("."); + var uiViewContext = uiViewsByFqn[fqnToFirstSegment].creationContext; + return vc.$uiViewContextAnchor === (uiViewContext && uiViewContext.name); + }; }; + return ViewService; + }()); + + /** + * @coreapi + * @module core + */ /** */ + /** + * Global router state + * + * This is where we hold the global mutable state such as current state, current + * params, current transition, etc. + */ + var UIRouterGlobals = /** @class */ (function () { + function UIRouterGlobals() { + /** + * Current parameter values + * + * The parameter values from the latest successful transition + */ + this.params = new StateParams(); + /** @internalapi */ + this.lastStartedTransitionId = -1; + /** @internalapi */ + this.transitionHistory = new Queue([], 1); + /** @internalapi */ + this.successfulTransitions = new Queue([], 1); + } + UIRouterGlobals.prototype.dispose = function () { + this.transitionHistory.clear(); + this.successfulTransitions.clear(); + this.transition = null; + }; + return UIRouterGlobals; + }()); + + /** + * @coreapi + * @module url + */ /** */ + /** @hidden */ + var makeStub = function (keys) { + return keys.reduce(function (acc, key) { return (acc[key] = notImplemented(key), acc); }, { dispose: noop$1 }); + }; + /** @hidden */ var locationServicesFns = ["url", "path", "search", "hash", "onChange"]; + /** @hidden */ var locationConfigFns = ["port", "protocol", "host", "baseHref", "html5Mode", "hashPrefix"]; + /** @hidden */ var umfFns = ["type", "caseInsensitive", "strictMode", "defaultSquashPolicy"]; + /** @hidden */ var rulesFns = ["sort", "when", "initial", "otherwise", "rules", "rule", "removeRule"]; + /** @hidden */ var syncFns = ["deferIntercept", "listen", "sync", "match"]; + /** + * API for URL management + */ + var UrlService = /** @class */ (function () { + /** @hidden */ + function UrlService(router, lateBind) { + if (lateBind === void 0) { lateBind = true; } + this.router = router; + this.rules = {}; + this.config = {}; + // proxy function calls from UrlService to the LocationService/LocationConfig + var locationServices = function () { return router.locationService; }; + createProxyFunctions(locationServices, this, locationServices, locationServicesFns, lateBind); + var locationConfig = function () { return router.locationConfig; }; + createProxyFunctions(locationConfig, this.config, locationConfig, locationConfigFns, lateBind); + var umf = function () { return router.urlMatcherFactory; }; + createProxyFunctions(umf, this.config, umf, umfFns); + var urlRouter = function () { return router.urlRouter; }; + createProxyFunctions(urlRouter, this.rules, urlRouter, rulesFns); + createProxyFunctions(urlRouter, this, urlRouter, syncFns); + } + UrlService.prototype.url = function (newurl, replace, state) { return; }; + + /** @inheritdoc */ + UrlService.prototype.path = function () { return; }; + + /** @inheritdoc */ + UrlService.prototype.search = function () { return; }; + + /** @inheritdoc */ + UrlService.prototype.hash = function () { return; }; + + /** @inheritdoc */ + UrlService.prototype.onChange = function (callback) { return; }; + + /** + * Returns the current URL parts + * + * This method returns the current URL components as a [[UrlParts]] object. + * + * @returns the current url parts + */ + UrlService.prototype.parts = function () { + return { path: this.path(), search: this.search(), hash: this.hash() }; + }; + UrlService.prototype.dispose = function () { }; + /** @inheritdoc */ + UrlService.prototype.sync = function (evt) { return; }; + /** @inheritdoc */ + UrlService.prototype.listen = function (enabled) { return; }; + + /** @inheritdoc */ + UrlService.prototype.deferIntercept = function (defer) { return; }; + /** @inheritdoc */ + UrlService.prototype.match = function (urlParts) { return; }; + /** @hidden */ + UrlService.locationServiceStub = makeStub(locationServicesFns); + /** @hidden */ + UrlService.locationConfigStub = makeStub(locationConfigFns); + return UrlService; + }()); + + /** + * @coreapi + * @module core + */ /** */ + /** @hidden */ + var _routerInstance = 0; + /** + * The master class used to instantiate an instance of UI-Router. + * + * UI-Router (for each specific framework) will create an instance of this class during bootstrap. + * This class instantiates and wires the UI-Router services together. + * + * After a new instance of the UIRouter class is created, it should be configured for your app. + * For instance, app states should be registered with the [[UIRouter.stateRegistry]]. + * + * --- + * + * Normally the framework code will bootstrap UI-Router. + * If you are bootstrapping UIRouter manually, tell it to monitor the URL by calling + * [[UrlService.listen]] then [[UrlService.sync]]. + */ + var UIRouter = /** @class */ (function () { + /** + * Creates a new `UIRouter` object + * + * @param locationService a [[LocationServices]] implementation + * @param locationConfig a [[LocationConfig]] implementation + * @internalapi + */ + function UIRouter(locationService, locationConfig) { + if (locationService === void 0) { locationService = UrlService.locationServiceStub; } + if (locationConfig === void 0) { locationConfig = UrlService.locationConfigStub; } + this.locationService = locationService; + this.locationConfig = locationConfig; + /** @hidden */ this.$id = _routerInstance++; + /** @hidden */ this._disposed = false; + /** @hidden */ this._disposables = []; + /** Provides trace information to the console */ + this.trace = trace; + /** Provides services related to ui-view synchronization */ + this.viewService = new ViewService(); + /** Provides services related to Transitions */ + this.transitionService = new TransitionService(this); + /** Global router state */ + this.globals = new UIRouterGlobals(); + /** + * Deprecated for public use. Use [[urlService]] instead. + * @deprecated Use [[urlService]] instead + */ + this.urlMatcherFactory = new UrlMatcherFactory(); + /** + * Deprecated for public use. Use [[urlService]] instead. + * @deprecated Use [[urlService]] instead + */ + this.urlRouter = new UrlRouter(this); + /** Provides a registry for states, and related registration services */ + this.stateRegistry = new StateRegistry(this); + /** Provides services related to states */ + this.stateService = new StateService(this); + /** Provides services related to the URL */ + this.urlService = new UrlService(this); + /** @hidden */ + this._plugins = {}; + this.viewService._pluginapi._rootViewContext(this.stateRegistry.root()); + this.globals.$current = this.stateRegistry.root(); + this.globals.current = this.globals.$current.self; + this.disposable(this.globals); + this.disposable(this.stateService); + this.disposable(this.stateRegistry); + this.disposable(this.transitionService); + this.disposable(this.urlRouter); + this.disposable(locationService); + this.disposable(locationConfig); + } + /** Registers an object to be notified when the router is disposed */ + UIRouter.prototype.disposable = function (disposable) { + this._disposables.push(disposable); + }; + /** + * Disposes this router instance + * + * When called, clears resources retained by the router by calling `dispose(this)` on all + * registered [[disposable]] objects. + * + * Or, if a `disposable` object is provided, calls `dispose(this)` on that object only. + * + * @param disposable (optional) the disposable to dispose + */ + UIRouter.prototype.dispose = function (disposable) { + var _this = this; + if (disposable && isFunction(disposable.dispose)) { + disposable.dispose(this); + return undefined; + } + this._disposed = true; + this._disposables.slice().forEach(function (d) { + try { + typeof d.dispose === 'function' && d.dispose(_this); + removeFrom(_this._disposables, d); + } + catch (ignored) { } + }); + }; + /** + * Adds a plugin to UI-Router + * + * This method adds a UI-Router Plugin. + * A plugin can enhance or change UI-Router behavior using any public API. + * + * #### Example: + * ```js + * import { MyCoolPlugin } from "ui-router-cool-plugin"; + * + * var plugin = router.addPlugin(MyCoolPlugin); + * ``` + * + * ### Plugin authoring + * + * A plugin is simply a class (or constructor function) which accepts a [[UIRouter]] instance and (optionally) an options object. + * + * The plugin can implement its functionality using any of the public APIs of [[UIRouter]]. + * For example, it may configure router options or add a Transition Hook. + * + * The plugin can then be published as a separate module. + * + * #### Example: + * ```js + * export class MyAuthPlugin implements UIRouterPlugin { + * constructor(router: UIRouter, options: any) { + * this.name = "MyAuthPlugin"; + * let $transitions = router.transitionService; + * let $state = router.stateService; + * + * let authCriteria = { + * to: (state) => state.data && state.data.requiresAuth + * }; + * + * function authHook(transition: Transition) { + * let authService = transition.injector().get('AuthService'); + * if (!authService.isAuthenticated()) { + * return $state.target('login'); + * } + * } + * + * $transitions.onStart(authCriteria, authHook); + * } + * } + * ``` + * + * @param plugin one of: + * - a plugin class which implements [[UIRouterPlugin]] + * - a constructor function for a [[UIRouterPlugin]] which accepts a [[UIRouter]] instance + * - a factory function which accepts a [[UIRouter]] instance and returns a [[UIRouterPlugin]] instance + * @param options options to pass to the plugin class/factory + * @returns the registered plugin instance + */ + UIRouter.prototype.plugin = function (plugin, options) { + if (options === void 0) { options = {}; } + var pluginInstance = new plugin(this, options); + if (!pluginInstance.name) + throw new Error("Required property `name` missing on plugin: " + pluginInstance); + this._disposables.push(pluginInstance); + return this._plugins[pluginInstance.name] = pluginInstance; + }; + UIRouter.prototype.getPlugin = function (pluginName) { + return pluginName ? this._plugins[pluginName] : values(this._plugins); + }; + return UIRouter; + }()); + + /** @module hooks */ /** */ + function addCoreResolvables(trans) { + trans.addResolvable({ token: UIRouter, deps: [], resolveFn: function () { return trans.router; }, data: trans.router }, ""); + trans.addResolvable({ token: Transition, deps: [], resolveFn: function () { return trans; }, data: trans }, ""); + trans.addResolvable({ token: '$transition$', deps: [], resolveFn: function () { return trans; }, data: trans }, ""); + trans.addResolvable({ token: '$stateParams', deps: [], resolveFn: function () { return trans.params(); }, data: trans.params() }, ""); + trans.entering().forEach(function (state) { + trans.addResolvable({ token: '$state$', deps: [], resolveFn: function () { return state; }, data: state }, state); + }); + } + var registerAddCoreResolvables = function (transitionService) { + return transitionService.onCreate({}, addCoreResolvables); + }; + + /** @module hooks */ /** */ + /** + * A [[TransitionHookFn]] that redirects to a different state or params + * + * Registered using `transitionService.onStart({ to: (state) => !!state.redirectTo }, redirectHook);` + * + * See [[StateDeclaration.redirectTo]] + */ + var redirectToHook = function (trans) { + var redirect = trans.to().redirectTo; + if (!redirect) + return; + var $state = trans.router.stateService; + function handleResult(result) { + if (!result) + return; + if (result instanceof TargetState) + return result; + if (isString(result)) + return $state.target(result, trans.params(), trans.options()); + if (result['state'] || result['params']) + return $state.target(result['state'] || trans.to(), result['params'] || trans.params(), trans.options()); + } + if (isFunction(redirect)) { + return services.$q.when(redirect(trans)).then(handleResult); + } + return handleResult(redirect); + }; + var registerRedirectToHook = function (transitionService) { + return transitionService.onStart({ to: function (state) { return !!state.redirectTo; } }, redirectToHook); + }; + + /** + * A factory which creates an onEnter, onExit or onRetain transition hook function + * + * The returned function invokes the (for instance) state.onEnter hook when the + * state is being entered. + * + * @hidden + */ + function makeEnterExitRetainHook(hookName) { + return function (transition, state) { + var _state = state.$$state(); + var hookFn = _state[hookName]; + return hookFn(transition, state); + }; + } + /** + * The [[TransitionStateHookFn]] for onExit + * + * When the state is being exited, the state's .onExit function is invoked. + * + * Registered using `transitionService.onExit({ exiting: (state) => !!state.onExit }, onExitHook);` + * + * See: [[IHookRegistry.onExit]] + */ + var onExitHook = makeEnterExitRetainHook('onExit'); + var registerOnExitHook = function (transitionService) { + return transitionService.onExit({ exiting: function (state) { return !!state.onExit; } }, onExitHook); + }; + /** + * The [[TransitionStateHookFn]] for onRetain + * + * When the state was already entered, and is not being exited or re-entered, the state's .onRetain function is invoked. + * + * Registered using `transitionService.onRetain({ retained: (state) => !!state.onRetain }, onRetainHook);` + * + * See: [[IHookRegistry.onRetain]] + */ + var onRetainHook = makeEnterExitRetainHook('onRetain'); + var registerOnRetainHook = function (transitionService) { + return transitionService.onRetain({ retained: function (state) { return !!state.onRetain; } }, onRetainHook); + }; + /** + * The [[TransitionStateHookFn]] for onEnter + * + * When the state is being entered, the state's .onEnter function is invoked. + * + * Registered using `transitionService.onEnter({ entering: (state) => !!state.onEnter }, onEnterHook);` + * + * See: [[IHookRegistry.onEnter]] + */ + var onEnterHook = makeEnterExitRetainHook('onEnter'); + var registerOnEnterHook = function (transitionService) { + return transitionService.onEnter({ entering: function (state) { return !!state.onEnter; } }, onEnterHook); + }; + + /** @module hooks */ + /** for typedoc */ + /** + * A [[TransitionHookFn]] which resolves all EAGER Resolvables in the To Path + * + * Registered using `transitionService.onStart({}, eagerResolvePath);` + * + * When a Transition starts, this hook resolves all the EAGER Resolvables, which the transition then waits for. + * + * See [[StateDeclaration.resolve]] + */ + var eagerResolvePath = function (trans) { + return new ResolveContext(trans.treeChanges().to) + .resolvePath("EAGER", trans) + .then(noop$1); + }; + var registerEagerResolvePath = function (transitionService) { + return transitionService.onStart({}, eagerResolvePath, { priority: 1000 }); + }; + /** + * A [[TransitionHookFn]] which resolves all LAZY Resolvables for the state (and all its ancestors) in the To Path + * + * Registered using `transitionService.onEnter({ entering: () => true }, lazyResolveState);` + * + * When a State is being entered, this hook resolves all the Resolvables for this state, which the transition then waits for. + * + * See [[StateDeclaration.resolve]] + */ + var lazyResolveState = function (trans, state) { + return new ResolveContext(trans.treeChanges().to) + .subContext(state.$$state()) + .resolvePath("LAZY", trans) + .then(noop$1); + }; + var registerLazyResolveState = function (transitionService) { + return transitionService.onEnter({ entering: val(true) }, lazyResolveState, { priority: 1000 }); + }; + + /** @module hooks */ /** for typedoc */ + /** + * A [[TransitionHookFn]] which waits for the views to load + * + * Registered using `transitionService.onStart({}, loadEnteringViews);` + * + * Allows the views to do async work in [[ViewConfig.load]] before the transition continues. + * In angular 1, this includes loading the templates. + */ + var loadEnteringViews = function (transition) { + var $q = services.$q; + var enteringViews = transition.views("entering"); + if (!enteringViews.length) + return; + return $q.all(enteringViews.map(function (view) { return $q.when(view.load()); })).then(noop$1); + }; + var registerLoadEnteringViews = function (transitionService) { + return transitionService.onFinish({}, loadEnteringViews); + }; + /** + * A [[TransitionHookFn]] which activates the new views when a transition is successful. + * + * Registered using `transitionService.onSuccess({}, activateViews);` + * + * After a transition is complete, this hook deactivates the old views from the previous state, + * and activates the new views from the destination state. + * + * See [[ViewService]] + */ + var activateViews = function (transition) { + var enteringViews = transition.views("entering"); + var exitingViews = transition.views("exiting"); + if (!enteringViews.length && !exitingViews.length) + return; + var $view = transition.router.viewService; + exitingViews.forEach(function (vc) { return $view.deactivateViewConfig(vc); }); + enteringViews.forEach(function (vc) { return $view.activateViewConfig(vc); }); + $view.sync(); + }; + var registerActivateViews = function (transitionService) { + return transitionService.onSuccess({}, activateViews); + }; + + /** + * A [[TransitionHookFn]] which updates global UI-Router state + * + * Registered using `transitionService.onBefore({}, updateGlobalState);` + * + * Before a [[Transition]] starts, updates the global value of "the current transition" ([[Globals.transition]]). + * After a successful [[Transition]], updates the global values of "the current state" + * ([[Globals.current]] and [[Globals.$current]]) and "the current param values" ([[Globals.params]]). + * + * See also the deprecated properties: + * [[StateService.transition]], [[StateService.current]], [[StateService.params]] + */ + var updateGlobalState = function (trans) { + var globals = trans.router.globals; + var transitionSuccessful = function () { + globals.successfulTransitions.enqueue(trans); + globals.$current = trans.$to(); + globals.current = globals.$current.self; + copy(trans.params(), globals.params); + }; + var clearCurrentTransition = function () { + // Do not clear globals.transition if a different transition has started in the meantime + if (globals.transition === trans) + globals.transition = null; + }; + trans.onSuccess({}, transitionSuccessful, { priority: 10000 }); + trans.promise.then(clearCurrentTransition, clearCurrentTransition); + }; + var registerUpdateGlobalState = function (transitionService) { + return transitionService.onCreate({}, updateGlobalState); + }; + + /** + * A [[TransitionHookFn]] which updates the URL after a successful transition + * + * Registered using `transitionService.onSuccess({}, updateUrl);` + */ + var updateUrl = function (transition) { + var options = transition.options(); + var $state = transition.router.stateService; + var $urlRouter = transition.router.urlRouter; + // Dont update the url in these situations: + // The transition was triggered by a URL sync (options.source === 'url') + // The user doesn't want the url to update (options.location === false) + // The destination state, and all parents have no navigable url + if (options.source !== 'url' && options.location && $state.$current.navigable) { + var urlOptions = { replace: options.location === 'replace' }; + $urlRouter.push($state.$current.navigable.url, $state.params, urlOptions); + } + $urlRouter.update(true); + }; + var registerUpdateUrl = function (transitionService) { + return transitionService.onSuccess({}, updateUrl, { priority: 9999 }); + }; + + /** + * A [[TransitionHookFn]] that performs lazy loading + * + * When entering a state "abc" which has a `lazyLoad` function defined: + * - Invoke the `lazyLoad` function (unless it is already in process) + * - Flag the hook function as "in process" + * - The function should return a promise (that resolves when lazy loading is complete) + * - Wait for the promise to settle + * - If the promise resolves to a [[LazyLoadResult]], then register those states + * - Flag the hook function as "not in process" + * - If the hook was successful + * - Remove the `lazyLoad` function from the state declaration + * - If all the hooks were successful + * - Retry the transition (by returning a TargetState) + * + * ``` + * .state('abc', { + * component: 'fooComponent', + * lazyLoad: () => System.import('./fooComponent') + * }); + * ``` + * + * See [[StateDeclaration.lazyLoad]] + */ + var lazyLoadHook = function (transition) { + var router = transition.router; + function retryTransition() { + if (transition.originalTransition().options().source !== 'url') { + // The original transition was not triggered via url sync + // The lazy state should be loaded now, so re-try the original transition + var orig = transition.targetState(); + return router.stateService.target(orig.identifier(), orig.params(), orig.options()); + } + // The original transition was triggered via url sync + // Run the URL rules and find the best match + var $url = router.urlService; + var result = $url.match($url.parts()); + var rule = result && result.rule; + // If the best match is a state, redirect the transition (instead + // of calling sync() which supersedes the current transition) + if (rule && rule.type === "STATE") { + var state = rule.state; + var params = result.match; + return router.stateService.target(state, params, transition.options()); + } + // No matching state found, so let .sync() choose the best non-state match/otherwise + router.urlService.sync(); + } + var promises = transition.entering() + .filter(function (state) { return !!state.$$state().lazyLoad; }) + .map(function (state) { return lazyLoadState(transition, state); }); + return services.$q.all(promises).then(retryTransition); + }; + var registerLazyLoadHook = function (transitionService) { + return transitionService.onBefore({ entering: function (state) { return !!state.lazyLoad; } }, lazyLoadHook); + }; + /** + * Invokes a state's lazy load function + * + * @param transition a Transition context + * @param state the state to lazy load + * @returns A promise for the lazy load result + */ + function lazyLoadState(transition, state) { + var lazyLoadFn = state.$$state().lazyLoad; + // Store/get the lazy load promise on/from the hookfn so it doesn't get re-invoked + var promise = lazyLoadFn['_promise']; + if (!promise) { + var success = function (result) { + delete state.lazyLoad; + delete state.$$state().lazyLoad; + delete lazyLoadFn['_promise']; + return result; + }; + var error = function (err) { + delete lazyLoadFn['_promise']; + return services.$q.reject(err); + }; + promise = lazyLoadFn['_promise'] = + services.$q.when(lazyLoadFn(transition, state)) + .then(updateStateRegistry) + .then(success, error); + } + /** Register any lazy loaded state definitions */ + function updateStateRegistry(result) { + if (result && Array.isArray(result.states)) { + result.states.forEach(function (state) { return transition.router.stateRegistry.register(state); }); + } + return result; + } + return promise; + } + + /** + * This class defines a type of hook, such as `onBefore` or `onEnter`. + * Plugins can define custom hook types, such as sticky states does for `onInactive`. + * + * @interalapi + */ + var TransitionEventType = /** @class */ (function () { + function TransitionEventType(name, hookPhase, hookOrder, criteriaMatchPath, reverseSort, getResultHandler, getErrorHandler, synchronous) { + if (reverseSort === void 0) { reverseSort = false; } + if (getResultHandler === void 0) { getResultHandler = TransitionHook.HANDLE_RESULT; } + if (getErrorHandler === void 0) { getErrorHandler = TransitionHook.REJECT_ERROR; } + if (synchronous === void 0) { synchronous = false; } + this.name = name; + this.hookPhase = hookPhase; + this.hookOrder = hookOrder; + this.criteriaMatchPath = criteriaMatchPath; + this.reverseSort = reverseSort; + this.getResultHandler = getResultHandler; + this.getErrorHandler = getErrorHandler; + this.synchronous = synchronous; + } + return TransitionEventType; + }()); + + /** @module hooks */ /** */ + /** + * A [[TransitionHookFn]] that skips a transition if it should be ignored + * + * This hook is invoked at the end of the onBefore phase. + * + * If the transition should be ignored (because no parameter or states changed) + * then the transition is ignored and not processed. + */ + function ignoredHook(trans) { + var ignoredReason = trans._ignoredReason(); + if (!ignoredReason) + return; + trace.traceTransitionIgnored(trans); + var pending = trans.router.globals.transition; + // The user clicked a link going back to the *current state* ('A') + // However, there is also a pending transition in flight (to 'B') + // Abort the transition to 'B' because the user now wants to be back at 'A'. + if (ignoredReason === 'SameAsCurrent' && pending) { + pending.abort(); + } + return Rejection.ignored().toPromise(); + } + var registerIgnoredTransitionHook = function (transitionService) { + return transitionService.onBefore({}, ignoredHook, { priority: -9999 }); + }; + + /** @module hooks */ /** */ + /** + * A [[TransitionHookFn]] that rejects the Transition if it is invalid + * + * This hook is invoked at the end of the onBefore phase. + * If the transition is invalid (for example, param values do not validate) + * then the transition is rejected. + */ + function invalidTransitionHook(trans) { + if (!trans.valid()) { + throw new Error(trans.error()); + } + } + var registerInvalidTransitionHook = function (transitionService) { + return transitionService.onBefore({}, invalidTransitionHook, { priority: -10000 }); + }; + + /** + * @coreapi + * @module transition + */ + /** for typedoc */ + /** + * The default [[Transition]] options. + * + * Include this object when applying custom defaults: + * let reloadOpts = { reload: true, notify: true } + * let options = defaults(theirOpts, customDefaults, defaultOptions); + */ + var defaultTransOpts = { + location: true, + relative: null, + inherit: false, + notify: true, + reload: false, + custom: {}, + current: function () { return null; }, + source: "unknown" + }; + /** + * This class provides services related to Transitions. + * + * - Most importantly, it allows global Transition Hooks to be registered. + * - It allows the default transition error handler to be set. + * - It also has a factory function for creating new [[Transition]] objects, (used internally by the [[StateService]]). + * + * At bootstrap, [[UIRouter]] creates a single instance (singleton) of this class. + */ + var TransitionService = /** @class */ (function () { + /** @hidden */ + function TransitionService(_router) { + /** @hidden */ + this._transitionCount = 0; + /** @hidden The transition hook types, such as `onEnter`, `onStart`, etc */ + this._eventTypes = []; + /** @hidden The registered transition hooks */ + this._registeredHooks = {}; + /** @hidden The paths on a criteria object */ + this._criteriaPaths = {}; + this._router = _router; + this.$view = _router.viewService; + this._deregisterHookFns = {}; + this._pluginapi = createProxyFunctions(val(this), {}, val(this), [ + '_definePathType', + '_defineEvent', + '_getPathTypes', + '_getEvents', + 'getHooks', + ]); + this._defineCorePaths(); + this._defineCoreEvents(); + this._registerCoreTransitionHooks(); + } + /** + * Registers a [[TransitionHookFn]], called *while a transition is being constructed*. + * + * Registers a transition lifecycle hook, which is invoked during transition construction. + * + * This low level hook should only be used by plugins. + * This can be a useful time for plugins to add resolves or mutate the transition as needed. + * The Sticky States plugin uses this hook to modify the treechanges. + * + * ### Lifecycle + * + * `onCreate` hooks are invoked *while a transition is being constructed*. + * + * ### Return value + * + * The hook's return value is ignored + * + * @internalapi + * @param criteria defines which Transitions the Hook should be invoked for. + * @param callback the hook function which will be invoked. + * @param options the registration options + * @returns a function which deregisters the hook. + */ + TransitionService.prototype.onCreate = function (criteria, callback, options) { return; }; + /** @inheritdoc */ + TransitionService.prototype.onBefore = function (criteria, callback, options) { return; }; + /** @inheritdoc */ + TransitionService.prototype.onStart = function (criteria, callback, options) { return; }; + /** @inheritdoc */ + TransitionService.prototype.onExit = function (criteria, callback, options) { return; }; + /** @inheritdoc */ + TransitionService.prototype.onRetain = function (criteria, callback, options) { return; }; + /** @inheritdoc */ + TransitionService.prototype.onEnter = function (criteria, callback, options) { return; }; + /** @inheritdoc */ + TransitionService.prototype.onFinish = function (criteria, callback, options) { return; }; + /** @inheritdoc */ + TransitionService.prototype.onSuccess = function (criteria, callback, options) { return; }; + /** @inheritdoc */ + TransitionService.prototype.onError = function (criteria, callback, options) { return; }; + /** + * dispose + * @internalapi + */ + TransitionService.prototype.dispose = function (router) { + values(this._registeredHooks).forEach(function (hooksArray) { return hooksArray.forEach(function (hook) { + hook._deregistered = true; + removeFrom(hooksArray, hook); + }); }); + }; + /** + * Creates a new [[Transition]] object + * + * This is a factory function for creating new Transition objects. + * It is used internally by the [[StateService]] and should generally not be called by application code. + * + * @param fromPath the path to the current state (the from state) + * @param targetState the target state (destination) + * @returns a Transition + */ + TransitionService.prototype.create = function (fromPath, targetState) { + return new Transition(fromPath, targetState, this._router); + }; + /** @hidden */ + TransitionService.prototype._defineCoreEvents = function () { + var Phase = exports.TransitionHookPhase; + var TH = TransitionHook; + var paths = this._criteriaPaths; + var NORMAL_SORT = false, REVERSE_SORT = true; + var ASYNCHRONOUS = false, SYNCHRONOUS = true; + this._defineEvent("onCreate", Phase.CREATE, 0, paths.to, NORMAL_SORT, TH.LOG_REJECTED_RESULT, TH.THROW_ERROR, SYNCHRONOUS); + this._defineEvent("onBefore", Phase.BEFORE, 0, paths.to); + this._defineEvent("onStart", Phase.RUN, 0, paths.to); + this._defineEvent("onExit", Phase.RUN, 100, paths.exiting, REVERSE_SORT); + this._defineEvent("onRetain", Phase.RUN, 200, paths.retained); + this._defineEvent("onEnter", Phase.RUN, 300, paths.entering); + this._defineEvent("onFinish", Phase.RUN, 400, paths.to); + this._defineEvent("onSuccess", Phase.SUCCESS, 0, paths.to, NORMAL_SORT, TH.LOG_REJECTED_RESULT, TH.LOG_ERROR, SYNCHRONOUS); + this._defineEvent("onError", Phase.ERROR, 0, paths.to, NORMAL_SORT, TH.LOG_REJECTED_RESULT, TH.LOG_ERROR, SYNCHRONOUS); + }; + /** @hidden */ + TransitionService.prototype._defineCorePaths = function () { + var STATE = exports.TransitionHookScope.STATE, TRANSITION = exports.TransitionHookScope.TRANSITION; + this._definePathType("to", TRANSITION); + this._definePathType("from", TRANSITION); + this._definePathType("exiting", STATE); + this._definePathType("retained", STATE); + this._definePathType("entering", STATE); + }; + /** @hidden */ + TransitionService.prototype._defineEvent = function (name, hookPhase, hookOrder, criteriaMatchPath, reverseSort, getResultHandler, getErrorHandler, synchronous) { + if (reverseSort === void 0) { reverseSort = false; } + if (getResultHandler === void 0) { getResultHandler = TransitionHook.HANDLE_RESULT; } + if (getErrorHandler === void 0) { getErrorHandler = TransitionHook.REJECT_ERROR; } + if (synchronous === void 0) { synchronous = false; } + var eventType = new TransitionEventType(name, hookPhase, hookOrder, criteriaMatchPath, reverseSort, getResultHandler, getErrorHandler, synchronous); + this._eventTypes.push(eventType); + makeEvent(this, this, eventType); + }; + + /** @hidden */ + TransitionService.prototype._getEvents = function (phase) { + var transitionHookTypes = isDefined(phase) ? + this._eventTypes.filter(function (type) { return type.hookPhase === phase; }) : + this._eventTypes.slice(); + return transitionHookTypes.sort(function (l, r) { + var cmpByPhase = l.hookPhase - r.hookPhase; + return cmpByPhase === 0 ? l.hookOrder - r.hookOrder : cmpByPhase; + }); + }; + /** + * Adds a Path to be used as a criterion against a TreeChanges path + * + * For example: the `exiting` path in [[HookMatchCriteria]] is a STATE scoped path. + * It was defined by calling `defineTreeChangesCriterion('exiting', TransitionHookScope.STATE)` + * Each state in the exiting path is checked against the criteria and returned as part of the match. + * + * Another example: the `to` path in [[HookMatchCriteria]] is a TRANSITION scoped path. + * It was defined by calling `defineTreeChangesCriterion('to', TransitionHookScope.TRANSITION)` + * Only the tail of the `to` path is checked against the criteria and returned as part of the match. + * + * @hidden + */ + TransitionService.prototype._definePathType = function (name, hookScope) { + this._criteriaPaths[name] = { name: name, scope: hookScope }; + }; + /** * @hidden */ + TransitionService.prototype._getPathTypes = function () { + return this._criteriaPaths; + }; + /** @hidden */ + TransitionService.prototype.getHooks = function (hookName) { + return this._registeredHooks[hookName]; + }; + /** @hidden */ + TransitionService.prototype._registerCoreTransitionHooks = function () { + var fns = this._deregisterHookFns; + fns.addCoreResolves = registerAddCoreResolvables(this); + fns.ignored = registerIgnoredTransitionHook(this); + fns.invalid = registerInvalidTransitionHook(this); + // Wire up redirectTo hook + fns.redirectTo = registerRedirectToHook(this); + // Wire up onExit/Retain/Enter state hooks + fns.onExit = registerOnExitHook(this); + fns.onRetain = registerOnRetainHook(this); + fns.onEnter = registerOnEnterHook(this); + // Wire up Resolve hooks + fns.eagerResolve = registerEagerResolvePath(this); + fns.lazyResolve = registerLazyResolveState(this); + // Wire up the View management hooks + fns.loadViews = registerLoadEnteringViews(this); + fns.activateViews = registerActivateViews(this); + // Updates global state after a transition + fns.updateGlobals = registerUpdateGlobalState(this); + // After globals.current is updated at priority: 10000 + fns.updateUrl = registerUpdateUrl(this); + // Lazy load state trees + fns.lazyLoad = registerLazyLoadHook(this); + }; + return TransitionService; + }()); + + /** + * @coreapi + * @module state + */ + /** */ + /** + * Provides state related service functions + * + * This class provides services related to ui-router states. + * An instance of this class is located on the global [[UIRouter]] object. + */ + var StateService = /** @class */ (function () { + /** @internalapi */ + function StateService(router) { + this.router = router; + /** @internalapi */ + this.invalidCallbacks = []; + /** @hidden */ + this._defaultErrorHandler = function $defaultErrorHandler($error$) { + if ($error$ instanceof Error && $error$.stack) { + console.error($error$); + console.error($error$.stack); + } + else if ($error$ instanceof Rejection) { + console.error($error$.toString()); + if ($error$.detail && $error$.detail.stack) + console.error($error$.detail.stack); + } + else { + console.error($error$); + } + }; + var getters = ['current', '$current', 'params', 'transition']; + var boundFns = Object.keys(StateService.prototype).filter(not(inArray(getters))); + createProxyFunctions(val(StateService.prototype), this, val(this), boundFns); + } + Object.defineProperty(StateService.prototype, "transition", { + /** + * The [[Transition]] currently in progress (or null) + * + * This is a passthrough through to [[UIRouterGlobals.transition]] + */ + get: function () { return this.router.globals.transition; }, + enumerable: true, + configurable: true + }); + Object.defineProperty(StateService.prototype, "params", { + /** + * The latest successful state parameters + * + * This is a passthrough through to [[UIRouterGlobals.params]] + */ + get: function () { return this.router.globals.params; }, + enumerable: true, + configurable: true + }); + Object.defineProperty(StateService.prototype, "current", { + /** + * The current [[StateDeclaration]] + * + * This is a passthrough through to [[UIRouterGlobals.current]] + */ + get: function () { return this.router.globals.current; }, + enumerable: true, + configurable: true + }); + Object.defineProperty(StateService.prototype, "$current", { + /** + * The current [[StateObject]] + * + * This is a passthrough through to [[UIRouterGlobals.$current]] + */ + get: function () { return this.router.globals.$current; }, + enumerable: true, + configurable: true + }); + /** @internalapi */ + StateService.prototype.dispose = function () { + this.defaultErrorHandler(noop$1); + this.invalidCallbacks = []; + }; + /** + * Handler for when [[transitionTo]] is called with an invalid state. + * + * Invokes the [[onInvalid]] callbacks, in natural order. + * Each callback's return value is checked in sequence until one of them returns an instance of TargetState. + * The results of the callbacks are wrapped in $q.when(), so the callbacks may return promises. + * + * If a callback returns an TargetState, then it is used as arguments to $state.transitionTo() and the result returned. + * + * @internalapi + */ + StateService.prototype._handleInvalidTargetState = function (fromPath, toState) { + var _this = this; + var fromState = PathUtils.makeTargetState(this.router.stateRegistry, fromPath); + var globals = this.router.globals; + var latestThing = function () { return globals.transitionHistory.peekTail(); }; + var latest = latestThing(); + var callbackQueue = new Queue(this.invalidCallbacks.slice()); + var injector = new ResolveContext(fromPath).injector(); + var checkForRedirect = function (result) { + if (!(result instanceof TargetState)) { + return; + } + var target = result; + // Recreate the TargetState, in case the state is now defined. + target = _this.target(target.identifier(), target.params(), target.options()); + if (!target.valid()) { + return Rejection.invalid(target.error()).toPromise(); + } + if (latestThing() !== latest) { + return Rejection.superseded().toPromise(); + } + return _this.transitionTo(target.identifier(), target.params(), target.options()); + }; + function invokeNextCallback() { + var nextCallback = callbackQueue.dequeue(); + if (nextCallback === undefined) + return Rejection.invalid(toState.error()).toPromise(); + var callbackResult = services.$q.when(nextCallback(toState, fromState, injector)); + return callbackResult.then(checkForRedirect).then(function (result) { return result || invokeNextCallback(); }); + } + return invokeNextCallback(); + }; + /** + * Registers an Invalid State handler + * + * Registers a [[OnInvalidCallback]] function to be invoked when [[StateService.transitionTo]] + * has been called with an invalid state reference parameter + * + * Example: + * ```js + * stateService.onInvalid(function(to, from, injector) { + * if (to.name() === 'foo') { + * let lazyLoader = injector.get('LazyLoadService'); + * return lazyLoader.load('foo') + * .then(() => stateService.target('foo')); + * } + * }); + * ``` + * + * @param {function} callback invoked when the toState is invalid + * This function receives the (invalid) toState, the fromState, and an injector. + * The function may optionally return a [[TargetState]] or a Promise for a TargetState. + * If one is returned, it is treated as a redirect. + * + * @returns a function which deregisters the callback + */ + StateService.prototype.onInvalid = function (callback) { + this.invalidCallbacks.push(callback); + return function deregisterListener() { + removeFrom(this.invalidCallbacks)(callback); + }.bind(this); + }; + /** + * Reloads the current state + * + * A method that force reloads the current state, or a partial state hierarchy. + * All resolves are re-resolved, and components reinstantiated. + * + * #### Example: + * ```js + * let app angular.module('app', ['ui.router']); + * + * app.controller('ctrl', function ($scope, $state) { + * $scope.reload = function(){ + * $state.reload(); + * } + * }); + * ``` + * + * Note: `reload()` is just an alias for: + * + * ```js + * $state.transitionTo($state.current, $state.params, { + * reload: true, inherit: false + * }); + * ``` + * + * @param reloadState A state name or a state object. + * If present, this state and all its children will be reloaded, but ancestors will not reload. + * + * #### Example: + * ```js + * //assuming app application consists of 3 states: 'contacts', 'contacts.detail', 'contacts.detail.item' + * //and current state is 'contacts.detail.item' + * let app angular.module('app', ['ui.router']); + * + * app.controller('ctrl', function ($scope, $state) { + * $scope.reload = function(){ + * //will reload 'contact.detail' and nested 'contact.detail.item' states + * $state.reload('contact.detail'); + * } + * }); + * ``` + * + * @returns A promise representing the state of the new transition. See [[StateService.go]] + */ + StateService.prototype.reload = function (reloadState) { + return this.transitionTo(this.current, this.params, { + reload: isDefined(reloadState) ? reloadState : true, + inherit: false, + notify: false, + }); + }; + + /** + * Transition to a different state and/or parameters + * + * Convenience method for transitioning to a new state. + * + * `$state.go` calls `$state.transitionTo` internally but automatically sets options to + * `{ location: true, inherit: true, relative: router.globals.$current, notify: true }`. + * This allows you to use either an absolute or relative `to` argument (because of `relative: router.globals.$current`). + * It also allows you to specify * only the parameters you'd like to update, while letting unspecified parameters + * inherit from the current parameter values (because of `inherit: true`). + * + * #### Example: + * ```js + * let app = angular.module('app', ['ui.router']); + * + * app.controller('ctrl', function ($scope, $state) { + * $scope.changeState = function () { + * $state.go('contact.detail'); + * }; + * }); + * ``` + * + * @param to Absolute state name, state object, or relative state path (relative to current state). + * + * Some examples: + * + * - `$state.go('contact.detail')` - will go to the `contact.detail` state + * - `$state.go('^')` - will go to the parent state + * - `$state.go('^.sibling')` - if current state is `home.child`, will go to the `home.sibling` state + * - `$state.go('.child.grandchild')` - if current state is home, will go to the `home.child.grandchild` state + * + * @param params A map of the parameters that will be sent to the state, will populate $stateParams. + * + * Any parameters that are not specified will be inherited from current parameter values (because of `inherit: true`). + * This allows, for example, going to a sibling state that shares parameters defined by a parent state. + * + * @param options Transition options + * + * @returns {promise} A promise representing the state of the new transition. + */ + StateService.prototype.go = function (to, params, options) { + var defautGoOpts = { relative: this.$current, inherit: true }; + var transOpts = defaults(options, defautGoOpts, defaultTransOpts); + return this.transitionTo(to, params, transOpts); + }; + + /** + * Creates a [[TargetState]] + * + * This is a factory method for creating a TargetState + * + * This may be returned from a Transition Hook to redirect a transition, for example. + */ + StateService.prototype.target = function (identifier, params, options) { + if (options === void 0) { options = {}; } + // If we're reloading, find the state object to reload from + if (isObject(options.reload) && !options.reload.name) + throw new Error('Invalid reload state object'); + var reg = this.router.stateRegistry; + options.reloadState = options.reload === true ? reg.root() : reg.matcher.find(options.reload, options.relative); + if (options.reload && !options.reloadState) + throw new Error("No such reload state '" + (isString(options.reload) ? options.reload : options.reload.name) + "'"); + return new TargetState(this.router.stateRegistry, identifier, params, options); + }; + + StateService.prototype.getCurrentPath = function () { + var _this = this; + var globals = this.router.globals; + var latestSuccess = globals.successfulTransitions.peekTail(); + var rootPath = function () { return [new PathNode(_this.router.stateRegistry.root())]; }; + return latestSuccess ? latestSuccess.treeChanges().to : rootPath(); + }; + /** + * Low-level method for transitioning to a new state. + * + * The [[go]] method (which uses `transitionTo` internally) is recommended in most situations. + * + * #### Example: + * ```js + * let app = angular.module('app', ['ui.router']); + * + * app.controller('ctrl', function ($scope, $state) { + * $scope.changeState = function () { + * $state.transitionTo('contact.detail'); + * }; + * }); + * ``` + * + * @param to State name or state object. + * @param toParams A map of the parameters that will be sent to the state, + * will populate $stateParams. + * @param options Transition options + * + * @returns A promise representing the state of the new transition. See [[go]] + */ + StateService.prototype.transitionTo = function (to, toParams, options) { + var _this = this; + if (toParams === void 0) { toParams = {}; } + if (options === void 0) { options = {}; } + var router = this.router; + var globals = router.globals; + options = defaults(options, defaultTransOpts); + var getCurrent = function () { + return globals.transition; + }; + options = extend(options, { current: getCurrent }); + var ref = this.target(to, toParams, options); + var currentPath = this.getCurrentPath(); + if (!ref.exists()) + return this._handleInvalidTargetState(currentPath, ref); + if (!ref.valid()) + return silentRejection(ref.error()); + /** + * Special handling for Ignored, Aborted, and Redirected transitions + * + * The semantics for the transition.run() promise and the StateService.transitionTo() + * promise differ. For instance, the run() promise may be rejected because it was + * IGNORED, but the transitionTo() promise is resolved because from the user perspective + * no error occurred. Likewise, the transition.run() promise may be rejected because of + * a Redirect, but the transitionTo() promise is chained to the new Transition's promise. + */ + var rejectedTransitionHandler = function (transition) { return function (error) { + if (error instanceof Rejection) { + var isLatest = router.globals.lastStartedTransitionId === transition.$id; + if (error.type === exports.RejectType.IGNORED) { + isLatest && router.urlRouter.update(); + // Consider ignored `Transition.run()` as a successful `transitionTo` + return services.$q.when(globals.current); + } + var detail = error.detail; + if (error.type === exports.RejectType.SUPERSEDED && error.redirected && detail instanceof TargetState) { + // If `Transition.run()` was redirected, allow the `transitionTo()` promise to resolve successfully + // by returning the promise for the new (redirect) `Transition.run()`. + var redirect = transition.redirect(detail); + return redirect.run().catch(rejectedTransitionHandler(redirect)); + } + if (error.type === exports.RejectType.ABORTED) { + isLatest && router.urlRouter.update(); + return services.$q.reject(error); + } + } + var errorHandler = _this.defaultErrorHandler(); + errorHandler(error); + return services.$q.reject(error); + }; }; + var transition = this.router.transitionService.create(currentPath, ref); + var transitionToPromise = transition.run().catch(rejectedTransitionHandler(transition)); + silenceUncaughtInPromise(transitionToPromise); // issue #2676 + // Return a promise for the transition, which also has the transition object on it. + return extend(transitionToPromise, { transition: transition }); + }; + + /** + * Checks if the current state *is* the provided state + * + * Similar to [[includes]] but only checks for the full state name. + * If params is supplied then it will be tested for strict equality against the current + * active params object, so all params must match with none missing and no extras. + * + * #### Example: + * ```js + * $state.$current.name = 'contacts.details.item'; + * + * // absolute name + * $state.is('contact.details.item'); // returns true + * $state.is(contactDetailItemStateObject); // returns true + * ``` + * + * // relative name (. and ^), typically from a template + * // E.g. from the 'contacts.details' template + * ```html + *
          Item
          + * ``` + * + * @param stateOrName The state name (absolute or relative) or state object you'd like to check. + * @param params A param object, e.g. `{sectionId: section.id}`, that you'd like + * to test against the current active state. + * @param options An options object. The options are: + * - `relative`: If `stateOrName` is a relative state name and `options.relative` is set, .is will + * test relative to `options.relative` state (or name). + * + * @returns Returns true if it is the state. + */ + StateService.prototype.is = function (stateOrName, params, options) { + options = defaults(options, { relative: this.$current }); + var state = this.router.stateRegistry.matcher.find(stateOrName, options.relative); + if (!isDefined(state)) + return undefined; + if (this.$current !== state) + return false; + if (!params) + return true; + var schema = state.parameters({ inherit: true, matchingKeys: params }); + return Param.equals(schema, Param.values(schema, params), this.params); + }; + + /** + * Checks if the current state *includes* the provided state + * + * A method to determine if the current active state is equal to or is the child of the + * state stateName. If any params are passed then they will be tested for a match as well. + * Not all the parameters need to be passed, just the ones you'd like to test for equality. + * + * #### Example when `$state.$current.name === 'contacts.details.item'` + * ```js + * // Using partial names + * $state.includes("contacts"); // returns true + * $state.includes("contacts.details"); // returns true + * $state.includes("contacts.details.item"); // returns true + * $state.includes("contacts.list"); // returns false + * $state.includes("about"); // returns false + * ``` + * + * #### Glob Examples when `* $state.$current.name === 'contacts.details.item.url'`: + * ```js + * $state.includes("*.details.*.*"); // returns true + * $state.includes("*.details.**"); // returns true + * $state.includes("**.item.**"); // returns true + * $state.includes("*.details.item.url"); // returns true + * $state.includes("*.details.*.url"); // returns true + * $state.includes("*.details.*"); // returns false + * $state.includes("item.**"); // returns false + * ``` + * + * @param stateOrName A partial name, relative name, glob pattern, + * or state object to be searched for within the current state name. + * @param params A param object, e.g. `{sectionId: section.id}`, + * that you'd like to test against the current active state. + * @param options An options object. The options are: + * - `relative`: If `stateOrName` is a relative state name and `options.relative` is set, .is will + * test relative to `options.relative` state (or name). + * + * @returns {boolean} Returns true if it does include the state + */ + StateService.prototype.includes = function (stateOrName, params, options) { + options = defaults(options, { relative: this.$current }); + var glob = isString(stateOrName) && Glob.fromString(stateOrName); + if (glob) { + if (!glob.matches(this.$current.name)) + return false; + stateOrName = this.$current.name; + } + var state = this.router.stateRegistry.matcher.find(stateOrName, options.relative), include = this.$current.includes; + if (!isDefined(state)) + return undefined; + if (!isDefined(include[state.name])) + return false; + if (!params) + return true; + var schema = state.parameters({ inherit: true, matchingKeys: params }); + return Param.equals(schema, Param.values(schema, params), this.params); + }; + + /** + * Generates a URL for a state and parameters + * + * Returns the url for the given state populated with the given params. + * + * #### Example: + * ```js + * expect($state.href("about.person", { person: "bob" })).toEqual("/about/bob"); + * ``` + * + * @param stateOrName The state name or state object you'd like to generate a url from. + * @param params An object of parameter values to fill the state's required parameters. + * @param options Options object. The options are: + * + * @returns {string} compiled state url + */ + StateService.prototype.href = function (stateOrName, params, options) { + var defaultHrefOpts = { + lossy: true, + inherit: true, + absolute: false, + relative: this.$current, + }; + options = defaults(options, defaultHrefOpts); + params = params || {}; + var state = this.router.stateRegistry.matcher.find(stateOrName, options.relative); + if (!isDefined(state)) + return null; + if (options.inherit) + params = this.params.$inherit(params, this.$current, state); + var nav = (state && options.lossy) ? state.navigable : state; + if (!nav || nav.url === undefined || nav.url === null) { + return null; + } + return this.router.urlRouter.href(nav.url, params, { + absolute: options.absolute, + }); + }; + + /** + * Sets or gets the default [[transitionTo]] error handler. + * + * The error handler is called when a [[Transition]] is rejected or when any error occurred during the Transition. + * This includes errors caused by resolves and transition hooks. + * + * Note: + * This handler does not receive certain Transition rejections. + * Redirected and Ignored Transitions are not considered to be errors by [[StateService.transitionTo]]. + * + * The built-in default error handler logs the error to the console. + * + * You can provide your own custom handler. + * + * #### Example: + * ```js + * stateService.defaultErrorHandler(function() { + * // Do not log transitionTo errors + * }); + * ``` + * + * @param handler a global error handler function + * @returns the current global error handler + */ + StateService.prototype.defaultErrorHandler = function (handler) { + return this._defaultErrorHandler = handler || this._defaultErrorHandler; + }; + StateService.prototype.get = function (stateOrName, base) { + var reg = this.router.stateRegistry; + if (arguments.length === 0) + return reg.get(); + return reg.get(stateOrName, base || this.$current); + }; + /** + * Lazy loads a state + * + * Explicitly runs a state's [[StateDeclaration.lazyLoad]] function. + * + * @param stateOrName the state that should be lazy loaded + * @param transition the optional Transition context to use (if the lazyLoad function requires an injector, etc) + * Note: If no transition is provided, a noop transition is created using the from the current state to the current state. + * This noop transition is not actually run. + * + * @returns a promise to lazy load + */ + StateService.prototype.lazyLoad = function (stateOrName, transition) { + var state = this.get(stateOrName); + if (!state || !state.lazyLoad) + throw new Error("Can not lazy load " + stateOrName); + var currentPath = this.getCurrentPath(); + var target = PathUtils.makeTargetState(this.router.stateRegistry, currentPath); + transition = transition || this.router.transitionService.create(currentPath, target); + return lazyLoadState(transition, state); + }; + return StateService; + }()); + + /** + * # Transition subsystem + * + * This module contains APIs related to a Transition. + * + * See: + * - [[TransitionService]] + * - [[Transition]] + * - [[HookFn]], [[TransitionHookFn]], [[TransitionStateHookFn]], [[HookMatchCriteria]], [[HookResult]] + * + * @coreapi + * @preferred + * @module transition + */ /** for typedoc */ + + /** + * @internalapi + * @module vanilla + */ + /** */ + /** + * An angular1-like promise api + * + * This object implements four methods similar to the + * [angular 1 promise api](https://docs.angularjs.org/api/ng/service/$q) + * + * UI-Router evolved from an angular 1 library to a framework agnostic library. + * However, some of the `@uirouter/core` code uses these ng1 style APIs to support ng1 style dependency injection. + * + * This API provides native ES6 promise support wrapped as a $q-like API. + * Internally, UI-Router uses this $q object to perform promise operations. + * The `angular-ui-router` (ui-router for angular 1) uses the $q API provided by angular. + * + * $q-like promise api + */ + var $q = { + /** Normalizes a value as a promise */ + when: function (val) { return new Promise(function (resolve, reject) { return resolve(val); }); }, + /** Normalizes a value as a promise rejection */ + reject: function (val) { return new Promise(function (resolve, reject) { reject(val); }); }, + /** @returns a deferred object, which has `resolve` and `reject` functions */ + defer: function () { + var deferred = {}; + deferred.promise = new Promise(function (resolve, reject) { + deferred.resolve = resolve; + deferred.reject = reject; + }); + return deferred; + }, + /** Like Promise.all(), but also supports object key/promise notation like $q */ + all: function (promises) { + if (isArray(promises)) { + return Promise.all(promises); + } + if (isObject(promises)) { + // Convert promises map to promises array. + // When each promise resolves, map it to a tuple { key: key, val: val } + var chain = Object.keys(promises) + .map(function (key) { return promises[key].then(function (val) { return ({ key: key, val: val }); }); }); + // Then wait for all promises to resolve, and convert them back to an object + return $q.all(chain).then(function (values) { + return values.reduce(function (acc, tuple) { acc[tuple.key] = tuple.val; return acc; }, {}); + }); + } + } + }; + + /** + * @internalapi + * @module vanilla + */ + /** */ +// globally available injectables + var globals = {}; + var STRIP_COMMENTS = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg; + var ARGUMENT_NAMES = /([^\s,]+)/g; + /** + * A basic angular1-like injector api + * + * This object implements four methods similar to the + * [angular 1 dependency injector](https://docs.angularjs.org/api/auto/service/$injector) + * + * UI-Router evolved from an angular 1 library to a framework agnostic library. + * However, some of the `@uirouter/core` code uses these ng1 style APIs to support ng1 style dependency injection. + * + * This object provides a naive implementation of a globally scoped dependency injection system. + * It supports the following DI approaches: + * + * ### Function parameter names + * + * A function's `.toString()` is called, and the parameter names are parsed. + * This only works when the parameter names aren't "mangled" by a minifier such as UglifyJS. + * + * ```js + * function injectedFunction(FooService, BarService) { + * // FooService and BarService are injected + * } + * ``` + * + * ### Function annotation + * + * A function may be annotated with an array of dependency names as the `$inject` property. + * + * ```js + * injectedFunction.$inject = [ 'FooService', 'BarService' ]; + * function injectedFunction(fs, bs) { + * // FooService and BarService are injected as fs and bs parameters + * } + * ``` + * + * ### Array notation + * + * An array provides the names of the dependencies to inject (as strings). + * The function is the last element of the array. + * + * ```js + * [ 'FooService', 'BarService', function (fs, bs) { + * // FooService and BarService are injected as fs and bs parameters + * }] + * ``` + * + * @type {$InjectorLike} + */ + var $injector = { + /** Gets an object from DI based on a string token */ + get: function (name) { return globals[name]; }, + /** Returns true if an object named `name` exists in global DI */ + has: function (name) { return $injector.get(name) != null; }, + /** + * Injects a function + * + * @param fn the function to inject + * @param context the function's `this` binding + * @param locals An object with additional DI tokens and values, such as `{ someToken: { foo: 1 } }` + */ + invoke: function (fn, context, locals) { + var all = extend({}, globals, locals || {}); + var params = $injector.annotate(fn); + var ensureExist = assertPredicate(function (key) { return all.hasOwnProperty(key); }, function (key) { return "DI can't find injectable: '" + key + "'"; }); + var args = params.filter(ensureExist).map(function (x) { return all[x]; }); + if (isFunction(fn)) + return fn.apply(context, args); + else + return fn.slice(-1)[0].apply(context, args); + }, + /** + * Returns a function's dependencies + * + * Analyzes a function (or array) and returns an array of DI tokens that the function requires. + * @return an array of `string`s + */ + annotate: function (fn) { + if (!isInjectable(fn)) + throw new Error("Not an injectable function: " + fn); + if (fn && fn.$inject) + return fn.$inject; + if (isArray(fn)) + return fn.slice(0, -1); + var fnStr = fn.toString().replace(STRIP_COMMENTS, ''); + var result = fnStr.slice(fnStr.indexOf('(') + 1, fnStr.indexOf(')')).match(ARGUMENT_NAMES); + return result || []; + } + }; + + /** + * @internalapi + * @module vanilla + */ + /** */ + var keyValsToObjectR = function (accum, _a) { + var key = _a[0], val = _a[1]; + if (!accum.hasOwnProperty(key)) { + accum[key] = val; + } + else if (isArray(accum[key])) { + accum[key].push(val); + } + else { + accum[key] = [accum[key], val]; + } + return accum; + }; + var getParams = function (queryString) { + return queryString.split("&").filter(identity).map(splitEqual).reduce(keyValsToObjectR, {}); + }; + function parseUrl$1(url) { + var orEmptyString = function (x) { return x || ""; }; + var _a = splitHash(url).map(orEmptyString), beforehash = _a[0], hash = _a[1]; + var _b = splitQuery(beforehash).map(orEmptyString), path = _b[0], search = _b[1]; + return { path: path, search: search, hash: hash, url: url }; + } + var buildUrl = function (loc) { + var path = loc.path(); + var searchObject = loc.search(); + var hash = loc.hash(); + var search = Object.keys(searchObject).map(function (key) { + var param = searchObject[key]; + var vals = isArray(param) ? param : [param]; + return vals.map(function (val) { return key + "=" + val; }); + }).reduce(unnestR, []).join("&"); + return path + (search ? "?" + search : "") + (hash ? "#" + hash : ""); + }; + function locationPluginFactory(name, isHtml5, serviceClass, configurationClass) { + return function (router) { + var service = router.locationService = new serviceClass(router); + var configuration = router.locationConfig = new configurationClass(router, isHtml5); + function dispose(router) { + router.dispose(service); + router.dispose(configuration); + } + return { name: name, service: service, configuration: configuration, dispose: dispose }; + }; + } + + /** + * @internalapi + * @module vanilla + */ /** */ + /** A base `LocationServices` */ + var BaseLocationServices = /** @class */ (function () { + function BaseLocationServices(router, fireAfterUpdate) { + var _this = this; + this.fireAfterUpdate = fireAfterUpdate; + this._listener = function (evt) { return _this._listeners.forEach(function (cb) { return cb(evt); }); }; + this._listeners = []; + this.hash = function () { return parseUrl$1(_this._get()).hash; }; + this.path = function () { return parseUrl$1(_this._get()).path; }; + this.search = function () { return getParams(parseUrl$1(_this._get()).search); }; + this._location = root.location; + this._history = root.history; + } + BaseLocationServices.prototype.url = function (url, replace) { + if (replace === void 0) { replace = true; } + if (isDefined(url) && url !== this._get()) { + this._set(null, null, url, replace); + if (this.fireAfterUpdate) { + this._listeners.forEach(function (cb) { return cb({ url: url }); }); + } + } + return buildUrl(this); + }; + BaseLocationServices.prototype.onChange = function (cb) { + var _this = this; + this._listeners.push(cb); + return function () { return removeFrom(_this._listeners, cb); }; + }; + BaseLocationServices.prototype.dispose = function (router) { + deregAll(this._listeners); + }; + return BaseLocationServices; + }()); + + var __extends = (undefined && undefined.__extends) || (function () { + var extendStatics = Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || + function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; }; + return function (d, b) { + extendStatics(d, b); + function __() { this.constructor = d; } + d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); + }; + })(); + /** + * @internalapi + * @module vanilla + */ + /** */ + /** A `LocationServices` that uses the browser hash "#" to get/set the current location */ + var HashLocationService = /** @class */ (function (_super) { + __extends(HashLocationService, _super); + function HashLocationService(router) { + var _this = _super.call(this, router, false) || this; + root.addEventListener('hashchange', _this._listener, false); + return _this; + } + HashLocationService.prototype._get = function () { + return trimHashVal(this._location.hash); + }; + HashLocationService.prototype._set = function (state, title, url, replace) { + this._location.hash = url; + }; + HashLocationService.prototype.dispose = function (router) { + _super.prototype.dispose.call(this, router); + root.removeEventListener('hashchange', this._listener); + }; + return HashLocationService; + }(BaseLocationServices)); + + var __extends$1 = (undefined && undefined.__extends) || (function () { + var extendStatics = Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || + function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; }; + return function (d, b) { + extendStatics(d, b); + function __() { this.constructor = d; } + d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); + }; + })(); + /** + * @internalapi + * @module vanilla + */ + /** */ + /** A `LocationServices` that gets/sets the current location from an in-memory object */ + var MemoryLocationService = /** @class */ (function (_super) { + __extends$1(MemoryLocationService, _super); + function MemoryLocationService(router) { + return _super.call(this, router, true) || this; + } + MemoryLocationService.prototype._get = function () { + return this._url; + }; + MemoryLocationService.prototype._set = function (state, title, url, replace) { + this._url = url; + }; + return MemoryLocationService; + }(BaseLocationServices)); + + var __extends$2 = (undefined && undefined.__extends) || (function () { + var extendStatics = Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || + function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; }; + return function (d, b) { + extendStatics(d, b); + function __() { this.constructor = d; } + d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); + }; + })(); + /** + * A `LocationServices` that gets/sets the current location using the browser's `location` and `history` apis + * + * Uses `history.pushState` and `history.replaceState` + */ + var PushStateLocationService = /** @class */ (function (_super) { + __extends$2(PushStateLocationService, _super); + function PushStateLocationService(router) { + var _this = _super.call(this, router, true) || this; + _this._config = router.urlService.config; + root.addEventListener('popstate', _this._listener, false); + return _this; + } + + /** + * Gets the base prefix without: + * - trailing slash + * - trailing filename + * - protocol and hostname + * + * If , this returns '/base'. + * If , this returns '/base'. + * + * See: https://html.spec.whatwg.org/dev/semantics.html#the-base-element + */ + PushStateLocationService.prototype._getBasePrefix = function () { + return stripFile(this._config.baseHref()); + }; + PushStateLocationService.prototype._get = function () { + var _a = this._location, pathname = _a.pathname, hash = _a.hash, search = _a.search; + search = splitQuery(search)[1]; // strip ? if found + hash = splitHash(hash)[1]; // strip # if found + var basePrefix = this._getBasePrefix(); + var exactMatch = pathname === this._config.baseHref(); + var startsWith = pathname.startsWith(basePrefix); + pathname = exactMatch ? '/' : startsWith ? pathname.substring(basePrefix.length) : pathname; + return pathname + (search ? '?' + search : '') + (hash ? '#' + hash : ''); + }; + PushStateLocationService.prototype._set = function (state, title, url, replace) { + var fullUrl = this._getBasePrefix() + url; + if (replace) { + this._history.replaceState(state, title, fullUrl); + } + else { + this._history.pushState(state, title, fullUrl); + } + }; + PushStateLocationService.prototype.dispose = function (router) { + _super.prototype.dispose.call(this, router); + root.removeEventListener('popstate', this._listener); + }; + return PushStateLocationService; + }(BaseLocationServices)); + + /** A `LocationConfig` mock that gets/sets all config from an in-memory object */ + var MemoryLocationConfig = /** @class */ (function () { + function MemoryLocationConfig() { + var _this = this; + this._baseHref = ''; + this._port = 80; + this._protocol = "http"; + this._host = "localhost"; + this._hashPrefix = ""; + this.port = function () { return _this._port; }; + this.protocol = function () { return _this._protocol; }; + this.host = function () { return _this._host; }; + this.baseHref = function () { return _this._baseHref; }; + this.html5Mode = function () { return false; }; + this.hashPrefix = function (newval) { return isDefined(newval) ? _this._hashPrefix = newval : _this._hashPrefix; }; + this.dispose = noop$1; + } + return MemoryLocationConfig; + }()); + + /** + * @internalapi + * @module vanilla + */ + /** */ + /** A `LocationConfig` that delegates to the browser's `location` object */ + var BrowserLocationConfig = /** @class */ (function () { + function BrowserLocationConfig(router, _isHtml5) { + if (_isHtml5 === void 0) { _isHtml5 = false; } + this._isHtml5 = _isHtml5; + this._baseHref = undefined; + this._hashPrefix = ""; + } + BrowserLocationConfig.prototype.port = function () { + if (location.port) { + return Number(location.port); + } + return this.protocol() === 'https' ? 443 : 80; + }; + BrowserLocationConfig.prototype.protocol = function () { + return location.protocol.replace(/:/g, ''); + }; + BrowserLocationConfig.prototype.host = function () { + return location.hostname; + }; + BrowserLocationConfig.prototype.html5Mode = function () { + return this._isHtml5; + }; + BrowserLocationConfig.prototype.hashPrefix = function (newprefix) { + return isDefined(newprefix) ? this._hashPrefix = newprefix : this._hashPrefix; + }; + + BrowserLocationConfig.prototype.baseHref = function (href) { + return isDefined(href) ? this._baseHref = href : + isDefined(this._baseHref) ? this._baseHref : this.applyDocumentBaseHref(); + }; + BrowserLocationConfig.prototype.applyDocumentBaseHref = function () { + var baseTag = document.getElementsByTagName("base")[0]; + return this._baseHref = baseTag ? baseTag.href.substr(location.origin.length) : ""; + }; + BrowserLocationConfig.prototype.dispose = function () { }; + return BrowserLocationConfig; + }()); + + /** + * @internalapi + * @module vanilla + */ + /** */ + function servicesPlugin(router) { + services.$injector = $injector; + services.$q = $q; + return { name: "vanilla.services", $q: $q, $injector: $injector, dispose: function () { return null; } }; + } + /** A `UIRouterPlugin` uses the browser hash to get/set the current location */ + var hashLocationPlugin = locationPluginFactory('vanilla.hashBangLocation', false, HashLocationService, BrowserLocationConfig); + /** A `UIRouterPlugin` that gets/sets the current location using the browser's `location` and `history` apis */ + var pushStateLocationPlugin = locationPluginFactory("vanilla.pushStateLocation", true, PushStateLocationService, BrowserLocationConfig); + /** A `UIRouterPlugin` that gets/sets the current location from an in-memory object */ + var memoryLocationPlugin = locationPluginFactory("vanilla.memoryLocation", false, MemoryLocationService, MemoryLocationConfig); + + /** + * @internalapi + * @module vanilla + */ + /** */ + + /** + * # Core classes and interfaces + * + * The classes and interfaces that are core to ui-router and do not belong + * to a more specific subsystem (such as resolve). + * + * @coreapi + * @preferred + * @module core + */ /** for typedoc */ + /** @internalapi */ + var UIRouterPluginBase = /** @class */ (function () { + function UIRouterPluginBase() { + } + UIRouterPluginBase.prototype.dispose = function (router) { }; + return UIRouterPluginBase; + }()); + + /** + * @coreapi + * @module common + */ /** */ + + + + var index$1 = Object.freeze({ + root: root, + fromJson: fromJson, + toJson: toJson, + forEach: forEach, + extend: extend, + equals: equals, + identity: identity, + noop: noop$1, + createProxyFunctions: createProxyFunctions, + inherit: inherit, + inArray: inArray, + _inArray: _inArray, + removeFrom: removeFrom, + _removeFrom: _removeFrom, + pushTo: pushTo, + _pushTo: _pushTo, + deregAll: deregAll, + defaults: defaults, + mergeR: mergeR, + ancestors: ancestors, + pick: pick, + omit: omit, + pluck: pluck, + filter: filter, + find: find, + mapObj: mapObj, + map: map, + values: values, + allTrueR: allTrueR, + anyTrueR: anyTrueR, + unnestR: unnestR, + flattenR: flattenR, + pushR: pushR, + uniqR: uniqR, + unnest: unnest, + flatten: flatten, + assertPredicate: assertPredicate, + assertMap: assertMap, + assertFn: assertFn, + pairs: pairs, + arrayTuples: arrayTuples, + applyPairs: applyPairs, + tail: tail, + copy: copy, + _extend: _extend, + silenceUncaughtInPromise: silenceUncaughtInPromise, + silentRejection: silentRejection, + notImplemented: notImplemented, + services: services, + Glob: Glob, + curry: curry, + compose: compose, + pipe: pipe, + prop: prop, + propEq: propEq, + parse: parse, + not: not, + and: and, + or: or, + all: all, + any: any, + is: is, + eq: eq, + val: val, + invoke: invoke, + pattern: pattern, + isUndefined: isUndefined, + isDefined: isDefined, + isNull: isNull, + isNullOrUndefined: isNullOrUndefined, + isFunction: isFunction, + isNumber: isNumber, + isString: isString, + isObject: isObject, + isArray: isArray, + isDate: isDate, + isRegExp: isRegExp, + isState: isState, + isInjectable: isInjectable, + isPromise: isPromise, + Queue: Queue, + maxLength: maxLength, + padString: padString, + kebobString: kebobString, + functionToString: functionToString, + fnToString: fnToString, + stringify: stringify, + beforeAfterSubstr: beforeAfterSubstr, + hostRegex: hostRegex, + stripFile: stripFile, + splitHash: splitHash, + splitQuery: splitQuery, + splitEqual: splitEqual, + trimHashVal: trimHashVal, + splitOnDelim: splitOnDelim, + joinNeighborsR: joinNeighborsR, + get Category () { return exports.Category; }, + Trace: Trace, + trace: trace, + get DefType () { return exports.DefType; }, + Param: Param, + ParamTypes: ParamTypes, + StateParams: StateParams, + ParamType: ParamType, + PathNode: PathNode, + PathUtils: PathUtils, + resolvePolicies: resolvePolicies, + defaultResolvePolicy: defaultResolvePolicy, + Resolvable: Resolvable, + NATIVE_INJECTOR_TOKEN: NATIVE_INJECTOR_TOKEN, + ResolveContext: ResolveContext, + resolvablesBuilder: resolvablesBuilder, + StateBuilder: StateBuilder, + StateObject: StateObject, + StateMatcher: StateMatcher, + StateQueueManager: StateQueueManager, + StateRegistry: StateRegistry, + StateService: StateService, + TargetState: TargetState, + get TransitionHookPhase () { return exports.TransitionHookPhase; }, + get TransitionHookScope () { return exports.TransitionHookScope; }, + HookBuilder: HookBuilder, + matchState: matchState, + RegisteredHook: RegisteredHook, + makeEvent: makeEvent, + get RejectType () { return exports.RejectType; }, + Rejection: Rejection, + Transition: Transition, + TransitionHook: TransitionHook, + TransitionEventType: TransitionEventType, + defaultTransOpts: defaultTransOpts, + TransitionService: TransitionService, + UrlMatcher: UrlMatcher, + UrlMatcherFactory: UrlMatcherFactory, + UrlRouter: UrlRouter, + UrlRuleFactory: UrlRuleFactory, + BaseUrlRule: BaseUrlRule, + UrlService: UrlService, + ViewService: ViewService, + UIRouterGlobals: UIRouterGlobals, + UIRouter: UIRouter, + $q: $q, + $injector: $injector, + BaseLocationServices: BaseLocationServices, + HashLocationService: HashLocationService, + MemoryLocationService: MemoryLocationService, + PushStateLocationService: PushStateLocationService, + MemoryLocationConfig: MemoryLocationConfig, + BrowserLocationConfig: BrowserLocationConfig, + keyValsToObjectR: keyValsToObjectR, + getParams: getParams, + parseUrl: parseUrl$1, + buildUrl: buildUrl, + locationPluginFactory: locationPluginFactory, + servicesPlugin: servicesPlugin, + hashLocationPlugin: hashLocationPlugin, + pushStateLocationPlugin: pushStateLocationPlugin, + memoryLocationPlugin: memoryLocationPlugin, + UIRouterPluginBase: UIRouterPluginBase + }); + + function getNg1ViewConfigFactory() { + var templateFactory = null; + return function (path, view) { + templateFactory = templateFactory || services.$injector.get("$templateFactory"); + return [new Ng1ViewConfig(path, view, templateFactory)]; + }; + } + var hasAnyKey = function (keys, obj) { + return keys.reduce(function (acc, key) { return acc || isDefined(obj[key]); }, false); + }; + /** + * This is a [[StateBuilder.builder]] function for angular1 `views`. + * + * When the [[StateBuilder]] builds a [[StateObject]] object from a raw [[StateDeclaration]], this builder + * handles the `views` property with logic specific to @uirouter/angularjs (ng1). + * + * If no `views: {}` property exists on the [[StateDeclaration]], then it creates the `views` object + * and applies the state-level configuration to a view named `$default`. + */ + function ng1ViewsBuilder(state) { + // Do not process root state + if (!state.parent) + return {}; + var tplKeys = ['templateProvider', 'templateUrl', 'template', 'notify', 'async'], ctrlKeys = ['controller', 'controllerProvider', 'controllerAs', 'resolveAs'], compKeys = ['component', 'bindings', 'componentProvider'], nonCompKeys = tplKeys.concat(ctrlKeys), allViewKeys = compKeys.concat(nonCompKeys); + // Do not allow a state to have both state-level props and also a `views: {}` property. + // A state without a `views: {}` property can declare properties for the `$default` view as properties of the state. + // However, the `$default` approach should not be mixed with a separate `views: ` block. + if (isDefined(state.views) && hasAnyKey(allViewKeys, state)) { + throw new Error("State '" + state.name + "' has a 'views' object. " + + "It cannot also have \"view properties\" at the state level. " + + "Move the following properties into a view (in the 'views' object): " + + (" " + allViewKeys.filter(function (key) { return isDefined(state[key]); }).join(", "))); + } + var views = {}, viewsObject = state.views || { "$default": pick(state, allViewKeys) }; + forEach(viewsObject, function (config, name) { + // Account for views: { "": { template... } } + name = name || "$default"; + // Account for views: { header: "headerComponent" } + if (isString(config)) + config = { component: config }; + // Make a shallow copy of the config object + config = extend({}, config); + // Do not allow a view to mix props for component-style view with props for template/controller-style view + if (hasAnyKey(compKeys, config) && hasAnyKey(nonCompKeys, config)) { + throw new Error("Cannot combine: " + compKeys.join("|") + " with: " + nonCompKeys.join("|") + " in stateview: '" + name + "@" + state.name + "'"); + } + config.resolveAs = config.resolveAs || '$resolve'; + config.$type = "ng1"; + config.$context = state; + config.$name = name; + var normalized = ViewService.normalizeUIViewTarget(config.$context, config.$name); + config.$uiViewName = normalized.uiViewName; + config.$uiViewContextAnchor = normalized.uiViewContextAnchor; + views[name] = config; + }); + return views; + } + var id$1 = 0; + var Ng1ViewConfig = /** @class */ (function () { + function Ng1ViewConfig(path, viewDecl, factory) { + var _this = this; + this.path = path; + this.viewDecl = viewDecl; + this.factory = factory; + this.$id = id$1++; + this.loaded = false; + this.getTemplate = function (uiView, context) { + return _this.component ? _this.factory.makeComponentTemplate(uiView, context, _this.component, _this.viewDecl.bindings) : _this.template; + }; + } + Ng1ViewConfig.prototype.load = function () { + var _this = this; + var $q = services.$q; + var context = new ResolveContext(this.path); + var params = this.path.reduce(function (acc, node) { return extend(acc, node.paramValues); }, {}); + var promises = { + template: $q.when(this.factory.fromConfig(this.viewDecl, params, context)), + controller: $q.when(this.getController(context)) + }; + return $q.all(promises).then(function (results) { + trace.traceViewServiceEvent("Loaded", _this); + _this.controller = results.controller; + extend(_this, results.template); // Either { template: "tpl" } or { component: "cmpName" } + return _this; + }); + }; + /** + * Gets the controller for a view configuration. + * + * @returns {Function|Promise.} Returns a controller, or a promise that resolves to a controller. + */ + Ng1ViewConfig.prototype.getController = function (context) { + var provider = this.viewDecl.controllerProvider; + if (!isInjectable(provider)) + return this.viewDecl.controller; + var deps = services.$injector.annotate(provider); + var providerFn = isArray(provider) ? tail(provider) : provider; + var resolvable = new Resolvable("", providerFn, deps); + return resolvable.get(context); + }; + return Ng1ViewConfig; + }()); + + /** @module view */ + /** for typedoc */ + /** + * Service which manages loading of templates from a ViewConfig. + */ + var TemplateFactory = /** @class */ (function () { + function TemplateFactory() { + var _this = this; + /** @hidden */ this._useHttp = ng.version.minor < 3; + /** @hidden */ this.$get = ['$http', '$templateCache', '$injector', function ($http, $templateCache, $injector) { + _this.$templateRequest = $injector.has && $injector.has('$templateRequest') && $injector.get('$templateRequest'); + _this.$http = $http; + _this.$templateCache = $templateCache; + return _this; + }]; + } + /** @hidden */ + TemplateFactory.prototype.useHttpService = function (value) { + this._useHttp = value; + }; + + /** + * Creates a template from a configuration object. + * + * @param config Configuration object for which to load a template. + * The following properties are search in the specified order, and the first one + * that is defined is used to create the template: + * + * @param params Parameters to pass to the template function. + * @param context The resolve context associated with the template's view + * + * @return {string|object} The template html as a string, or a promise for + * that string,or `null` if no template is configured. + */ + TemplateFactory.prototype.fromConfig = function (config, params, context) { + var defaultTemplate = ""; + var asTemplate = function (result) { return services.$q.when(result).then(function (str) { return ({ template: str }); }); }; + var asComponent = function (result) { return services.$q.when(result).then(function (str) { return ({ component: str }); }); }; + return (isDefined(config.template) ? asTemplate(this.fromString(config.template, params)) : + isDefined(config.templateUrl) ? asTemplate(this.fromUrl(config.templateUrl, params)) : + isDefined(config.templateProvider) ? asTemplate(this.fromProvider(config.templateProvider, params, context)) : + isDefined(config.component) ? asComponent(config.component) : + isDefined(config.componentProvider) ? asComponent(this.fromComponentProvider(config.componentProvider, params, context)) : + asTemplate(defaultTemplate)); + }; + + /** + * Creates a template from a string or a function returning a string. + * + * @param template html template as a string or function that returns an html template as a string. + * @param params Parameters to pass to the template function. + * + * @return {string|object} The template html as a string, or a promise for that + * string. + */ + TemplateFactory.prototype.fromString = function (template, params) { + return isFunction(template) ? template(params) : template; + }; + + /** + * Loads a template from the a URL via `$http` and `$templateCache`. + * + * @param {string|Function} url url of the template to load, or a function + * that returns a url. + * @param {Object} params Parameters to pass to the url function. + * @return {string|Promise.} The template html as a string, or a promise + * for that string. + */ + TemplateFactory.prototype.fromUrl = function (url, params) { + if (isFunction(url)) + url = url(params); + if (url == null) + return null; + if (this._useHttp) { + return this.$http.get(url, { cache: this.$templateCache, headers: { Accept: 'text/html' } }) + .then(function (response) { + return response.data; + }); + } + return this.$templateRequest(url); + }; + + /** + * Creates a template by invoking an injectable provider function. + * + * @param provider Function to invoke via `locals` + * @param {Function} injectFn a function used to invoke the template provider + * @return {string|Promise.} The template html as a string, or a promise + * for that string. + */ + TemplateFactory.prototype.fromProvider = function (provider, params, context) { + var deps = services.$injector.annotate(provider); + var providerFn = isArray(provider) ? tail(provider) : provider; + var resolvable = new Resolvable("", providerFn, deps); + return resolvable.get(context); + }; + + /** + * Creates a component's template by invoking an injectable provider function. + * + * @param provider Function to invoke via `locals` + * @param {Function} injectFn a function used to invoke the template provider + * @return {string} The template html as a string: "". + */ + TemplateFactory.prototype.fromComponentProvider = function (provider, params, context) { + var deps = services.$injector.annotate(provider); + var providerFn = isArray(provider) ? tail(provider) : provider; + var resolvable = new Resolvable("", providerFn, deps); + return resolvable.get(context); + }; + + /** + * Creates a template from a component's name + * + * This implements route-to-component. + * It works by retrieving the component (directive) metadata from the injector. + * It analyses the component's bindings, then constructs a template that instantiates the component. + * The template wires input and output bindings to resolves or from the parent component. + * + * @param uiView {object} The parent ui-view (for binding outputs to callbacks) + * @param context The ResolveContext (for binding outputs to callbacks returned from resolves) + * @param component {string} Component's name in camel case. + * @param bindings An object defining the component's bindings: {foo: '<'} + * @return {string} The template as a string: "". + */ + TemplateFactory.prototype.makeComponentTemplate = function (uiView, context, component, bindings) { + bindings = bindings || {}; + // Bind once prefix + var prefix = ng.version.minor >= 3 ? "::" : ""; + // Convert to kebob name. Add x- prefix if the string starts with `x-` or `data-` + var kebob = function (camelCase) { + var kebobed = kebobString(camelCase); + return /^(x|data)-/.exec(kebobed) ? "x-" + kebobed : kebobed; + }; + var attributeTpl = function (input) { + var name = input.name, type = input.type; + var attrName = kebob(name); + // If the ui-view has an attribute which matches a binding on the routed component + // then pass that attribute through to the routed component template. + // Prefer ui-view wired mappings to resolve data, unless the resolve was explicitly bound using `bindings:` + if (uiView.attr(attrName) && !bindings[name]) + return attrName + "='" + uiView.attr(attrName) + "'"; + var resolveName = bindings[name] || name; + // Pre-evaluate the expression for "@" bindings by enclosing in {{ }} + // some-attr="{{ ::$resolve.someResolveName }}" + if (type === '@') + return attrName + "='{{" + prefix + "$resolve." + resolveName + "}}'"; + // Wire "&" callbacks to resolves that return a callback function + // Get the result of the resolve (should be a function) and annotate it to get its arguments. + // some-attr="$resolve.someResolveResultName(foo, bar)" + if (type === '&') { + var res = context.getResolvable(resolveName); + var fn = res && res.data; + var args = fn && services.$injector.annotate(fn) || []; + // account for array style injection, i.e., ['foo', function(foo) {}] + var arrayIdxStr = isArray(fn) ? "[" + (fn.length - 1) + "]" : ''; + return attrName + "='$resolve." + resolveName + arrayIdxStr + "(" + args.join(",") + ")'"; + } + // some-attr="::$resolve.someResolveName" + return attrName + "='" + prefix + "$resolve." + resolveName + "'"; + }; + var attrs = getComponentBindings(component).map(attributeTpl).join(" "); + var kebobName = kebob(component); + return "<" + kebobName + " " + attrs + ">"; + }; + + return TemplateFactory; + }()); +// Gets all the directive(s)' inputs ('@', '=', and '<') and outputs ('&') + function getComponentBindings(name) { + var cmpDefs = services.$injector.get(name + "Directive"); // could be multiple + if (!cmpDefs || !cmpDefs.length) + throw new Error("Unable to find component named '" + name + "'"); + return cmpDefs.map(getBindings).reduce(unnestR, []); + } +// Given a directive definition, find its object input attributes +// Use different properties, depending on the type of directive (component, bindToController, normal) + var getBindings = function (def) { + if (isObject(def.bindToController)) + return scopeBindings(def.bindToController); + return scopeBindings(def.scope); + }; +// for ng 1.2 style, process the scope: { input: "=foo" } +// for ng 1.3 through ng 1.5, process the component's bindToController: { input: "=foo" } object + var scopeBindings = function (bindingsObj) { return Object.keys(bindingsObj || {}) + .map(function (key) { return [key, /^([=<@&])[?]?(.*)/.exec(bindingsObj[key])]; }) + .filter(function (tuple) { return isDefined(tuple) && isArray(tuple[1]); }) + .map(function (tuple) { return ({ name: tuple[1][2] || tuple[0], type: tuple[1][1] }); }); }; + + /** @module ng1 */ /** for typedoc */ + /** + * The Angular 1 `StateProvider` + * + * The `$stateProvider` works similar to Angular's v1 router, but it focuses purely + * on state. + * + * A state corresponds to a "place" in the application in terms of the overall UI and + * navigation. A state describes (via the controller / template / view properties) what + * the UI looks like and does at that place. + * + * States often have things in common, and the primary way of factoring out these + * commonalities in this model is via the state hierarchy, i.e. parent/child states aka + * nested states. + * + * The `$stateProvider` provides interfaces to declare these states for your app. + */ + var StateProvider = /** @class */ (function () { + function StateProvider(stateRegistry, stateService) { + this.stateRegistry = stateRegistry; + this.stateService = stateService; + createProxyFunctions(val(StateProvider.prototype), this, val(this)); + } + /** + * Decorates states when they are registered + * + * Allows you to extend (carefully) or override (at your own peril) the + * `stateBuilder` object used internally by [[StateRegistry]]. + * This can be used to add custom functionality to ui-router, + * for example inferring templateUrl based on the state name. + * + * When passing only a name, it returns the current (original or decorated) builder + * function that matches `name`. + * + * The builder functions that can be decorated are listed below. Though not all + * necessarily have a good use case for decoration, that is up to you to decide. + * + * In addition, users can attach custom decorators, which will generate new + * properties within the state's internal definition. There is currently no clear + * use-case for this beyond accessing internal states (i.e. $state.$current), + * however, expect this to become increasingly relevant as we introduce additional + * meta-programming features. + * + * **Warning**: Decorators should not be interdependent because the order of + * execution of the builder functions in non-deterministic. Builder functions + * should only be dependent on the state definition object and super function. + * + * + * Existing builder functions and current return values: + * + * - **parent** `{object}` - returns the parent state object. + * - **data** `{object}` - returns state data, including any inherited data that is not + * overridden by own values (if any). + * - **url** `{object}` - returns a {@link ui.router.util.type:UrlMatcher UrlMatcher} + * or `null`. + * - **navigable** `{object}` - returns closest ancestor state that has a URL (aka is + * navigable). + * - **params** `{object}` - returns an array of state params that are ensured to + * be a super-set of parent's params. + * - **views** `{object}` - returns a views object where each key is an absolute view + * name (i.e. "viewName@stateName") and each value is the config object + * (template, controller) for the view. Even when you don't use the views object + * explicitly on a state config, one is still created for you internally. + * So by decorating this builder function you have access to decorating template + * and controller properties. + * - **ownParams** `{object}` - returns an array of params that belong to the state, + * not including any params defined by ancestor states. + * - **path** `{string}` - returns the full path from the root down to this state. + * Needed for state activation. + * - **includes** `{object}` - returns an object that includes every state that + * would pass a `$state.includes()` test. + * + * #### Example: + * Override the internal 'views' builder with a function that takes the state + * definition, and a reference to the internal function being overridden: + * ```js + * $stateProvider.decorator('views', function (state, parent) { + * let result = {}, + * views = parent(state); + * + * angular.forEach(views, function (config, name) { + * let autoName = (state.name + '.' + name).replace('.', '/'); + * config.templateUrl = config.templateUrl || '/partials/' + autoName + '.html'; + * result[name] = config; + * }); + * return result; + * }); + * + * $stateProvider.state('home', { + * views: { + * 'contact.list': { controller: 'ListController' }, + * 'contact.item': { controller: 'ItemController' } + * } + * }); + * ``` + * + * + * ```js + * // Auto-populates list and item views with /partials/home/contact/list.html, + * // and /partials/home/contact/item.html, respectively. + * $state.go('home'); + * ``` + * + * @param {string} name The name of the builder function to decorate. + * @param {object} func A function that is responsible for decorating the original + * builder function. The function receives two parameters: + * + * - `{object}` - state - The state config object. + * - `{object}` - super - The original builder function. + * + * @return {object} $stateProvider - $stateProvider instance + */ + StateProvider.prototype.decorator = function (name, func) { + return this.stateRegistry.decorator(name, func) || this; + }; + StateProvider.prototype.state = function (name, definition) { + if (isObject(name)) { + definition = name; + } + else { + definition.name = name; + } + this.stateRegistry.register(definition); + return this; + }; + /** + * Registers an invalid state handler + * + * This is a passthrough to [[StateService.onInvalid]] for ng1. + */ + StateProvider.prototype.onInvalid = function (callback) { + return this.stateService.onInvalid(callback); + }; + return StateProvider; + }()); + + /** @module ng1 */ /** */ + /** + * This is a [[StateBuilder.builder]] function for angular1 `onEnter`, `onExit`, + * `onRetain` callback hooks on a [[Ng1StateDeclaration]]. + * + * When the [[StateBuilder]] builds a [[StateObject]] object from a raw [[StateDeclaration]], this builder + * ensures that those hooks are injectable for @uirouter/angularjs (ng1). + */ + var getStateHookBuilder = function (hookName) { + return function stateHookBuilder(state, parentFn) { + var hook = state[hookName]; + var pathname = hookName === 'onExit' ? 'from' : 'to'; + function decoratedNg1Hook(trans, state) { + var resolveContext = new ResolveContext(trans.treeChanges(pathname)); + var locals = extend(getLocals(resolveContext), { $state$: state, $transition$: trans }); + return services.$injector.invoke(hook, this, locals); + } + return hook ? decoratedNg1Hook : undefined; + }; + }; + + /** + * Implements UI-Router LocationServices and LocationConfig using Angular 1's $location service + */ + var Ng1LocationServices = /** @class */ (function () { + function Ng1LocationServices($locationProvider) { + // .onChange() registry + this._urlListeners = []; + this.$locationProvider = $locationProvider; + var _lp = val($locationProvider); + createProxyFunctions(_lp, this, _lp, ['hashPrefix']); + } + Ng1LocationServices.prototype.dispose = function () { }; + Ng1LocationServices.prototype.onChange = function (callback) { + var _this = this; + this._urlListeners.push(callback); + return function () { return removeFrom(_this._urlListeners)(callback); }; + }; + Ng1LocationServices.prototype.html5Mode = function () { + var html5Mode = this.$locationProvider.html5Mode(); + html5Mode = isObject(html5Mode) ? html5Mode.enabled : html5Mode; + return html5Mode && this.$sniffer.history; + }; + Ng1LocationServices.prototype.url = function (newUrl, replace, state) { + if (replace === void 0) { replace = false; } + if (newUrl) + this.$location.url(newUrl); + if (replace) + this.$location.replace(); + if (state) + this.$location.state(state); + return this.$location.url(); + }; + Ng1LocationServices.prototype._runtimeServices = function ($rootScope, $location, $sniffer, $browser) { + var _this = this; + this.$location = $location; + this.$sniffer = $sniffer; + // Bind $locationChangeSuccess to the listeners registered in LocationService.onChange + $rootScope.$on("$locationChangeSuccess", function (evt) { return _this._urlListeners.forEach(function (fn) { return fn(evt); }); }); + var _loc = val($location); + var _browser = val($browser); + // Bind these LocationService functions to $location + createProxyFunctions(_loc, this, _loc, ["replace", "path", "search", "hash"]); + // Bind these LocationConfig functions to $location + createProxyFunctions(_loc, this, _loc, ['port', 'protocol', 'host']); + // Bind these LocationConfig functions to $browser + createProxyFunctions(_browser, this, _browser, ['baseHref']); + }; + /** + * Applys ng1-specific path parameter encoding + * + * The Angular 1 `$location` service is a bit weird. + * It doesn't allow slashes to be encoded/decoded bi-directionally. + * + * See the writeup at https://github.com/angular-ui/ui-router/issues/2598 + * + * This code patches the `path` parameter type so it encoded/decodes slashes as ~2F + * + * @param router + */ + Ng1LocationServices.monkeyPatchPathParameterType = function (router) { + var pathType = router.urlMatcherFactory.type('path'); + pathType.encode = function (val) { + return val != null ? val.toString().replace(/(~|\/)/g, function (m) { return ({ '~': '~~', '/': '~2F' }[m]); }) : val; + }; + pathType.decode = function (val) { + return val != null ? val.toString().replace(/(~~|~2F)/g, function (m) { return ({ '~~': '~', '~2F': '/' }[m]); }) : val; + }; + }; + return Ng1LocationServices; + }()); + + /** @module url */ /** */ + /** + * Manages rules for client-side URL + * + * ### Deprecation warning: + * This class is now considered to be an internal API + * Use the [[UrlService]] instead. + * For configuring URL rules, use the [[UrlRulesApi]] which can be found as [[UrlService.rules]]. + * + * This class manages the router rules for what to do when the URL changes. + * + * This provider remains for backwards compatibility. + * + * @deprecated + */ + var UrlRouterProvider = /** @class */ (function () { + /** @hidden */ + function UrlRouterProvider(router) { + this._router = router; + this._urlRouter = router.urlRouter; + } + /** @hidden */ + UrlRouterProvider.prototype.$get = function () { + var urlRouter = this._urlRouter; + urlRouter.update(true); + if (!urlRouter.interceptDeferred) + urlRouter.listen(); + return urlRouter; + }; + /** + * Registers a url handler function. + * + * Registers a low level url handler (a `rule`). + * A rule detects specific URL patterns and returns a redirect, or performs some action. + * + * If a rule returns a string, the URL is replaced with the string, and all rules are fired again. + * + * #### Example: + * ```js + * var app = angular.module('app', ['ui.router.router']); + * + * app.config(function ($urlRouterProvider) { + * // Here's an example of how you might allow case insensitive urls + * $urlRouterProvider.rule(function ($injector, $location) { + * var path = $location.path(), + * normalized = path.toLowerCase(); + * + * if (path !== normalized) { + * return normalized; + * } + * }); + * }); + * ``` + * + * @param ruleFn + * Handler function that takes `$injector` and `$location` services as arguments. + * You can use them to detect a url and return a different url as a string. + * + * @return [[UrlRouterProvider]] (`this`) + */ + UrlRouterProvider.prototype.rule = function (ruleFn) { + var _this = this; + if (!isFunction(ruleFn)) + throw new Error("'rule' must be a function"); + var match = function () { + return ruleFn(services.$injector, _this._router.locationService); + }; + var rule = new BaseUrlRule(match, identity); + this._urlRouter.rule(rule); + return this; + }; + + /** + * Defines the path or behavior to use when no url can be matched. + * + * #### Example: + * ```js + * var app = angular.module('app', ['ui.router.router']); + * + * app.config(function ($urlRouterProvider) { + * // if the path doesn't match any of the urls you configured + * // otherwise will take care of routing the user to the + * // specified url + * $urlRouterProvider.otherwise('/index'); + * + * // Example of using function rule as param + * $urlRouterProvider.otherwise(function ($injector, $location) { + * return '/a/valid/url'; + * }); + * }); + * ``` + * + * @param rule + * The url path you want to redirect to or a function rule that returns the url path or performs a `$state.go()`. + * The function version is passed two params: `$injector` and `$location` services, and should return a url string. + * + * @return {object} `$urlRouterProvider` - `$urlRouterProvider` instance + */ + UrlRouterProvider.prototype.otherwise = function (rule) { + var _this = this; + var urlRouter = this._urlRouter; + if (isString(rule)) { + urlRouter.otherwise(rule); + } + else if (isFunction(rule)) { + urlRouter.otherwise(function () { return rule(services.$injector, _this._router.locationService); }); + } + else { + throw new Error("'rule' must be a string or function"); + } + return this; + }; + + /** + * Registers a handler for a given url matching. + * + * If the handler is a string, it is + * treated as a redirect, and is interpolated according to the syntax of match + * (i.e. like `String.replace()` for `RegExp`, or like a `UrlMatcher` pattern otherwise). + * + * If the handler is a function, it is injectable. + * It gets invoked if `$location` matches. + * You have the option of inject the match object as `$match`. + * + * The handler can return + * + * - **falsy** to indicate that the rule didn't match after all, then `$urlRouter` + * will continue trying to find another one that matches. + * - **string** which is treated as a redirect and passed to `$location.url()` + * - **void** or any **truthy** value tells `$urlRouter` that the url was handled. + * + * #### Example: + * ```js + * var app = angular.module('app', ['ui.router.router']); + * + * app.config(function ($urlRouterProvider) { + * $urlRouterProvider.when($state.url, function ($match, $stateParams) { + * if ($state.$current.navigable !== state || + * !equalForKeys($match, $stateParams) { + * $state.transitionTo(state, $match, false); + * } + * }); + * }); + * ``` + * + * @param what A pattern string to match, compiled as a [[UrlMatcher]]. + * @param handler The path (or function that returns a path) that you want to redirect your user to. + * @param ruleCallback [optional] A callback that receives the `rule` registered with [[UrlMatcher.rule]] + * + * Note: the handler may also invoke arbitrary code, such as `$state.go()` + */ + UrlRouterProvider.prototype.when = function (what, handler) { + if (isArray(handler) || isFunction(handler)) { + handler = UrlRouterProvider.injectableHandler(this._router, handler); + } + this._urlRouter.when(what, handler); + return this; + }; + + UrlRouterProvider.injectableHandler = function (router, handler) { + return function (match) { + return services.$injector.invoke(handler, null, { $match: match, $stateParams: router.globals.params }); + }; + }; + /** + * Disables monitoring of the URL. + * + * Call this method before UI-Router has bootstrapped. + * It will stop UI-Router from performing the initial url sync. + * + * This can be useful to perform some asynchronous initialization before the router starts. + * Once the initialization is complete, call [[listen]] to tell UI-Router to start watching and synchronizing the URL. + * + * #### Example: + * ```js + * var app = angular.module('app', ['ui.router']); + * + * app.config(function ($urlRouterProvider) { + * // Prevent $urlRouter from automatically intercepting URL changes; + * $urlRouterProvider.deferIntercept(); + * }) + * + * app.run(function (MyService, $urlRouter, $http) { + * $http.get("/stuff").then(function(resp) { + * MyService.doStuff(resp.data); + * $urlRouter.listen(); + * $urlRouter.sync(); + * }); + * }); + * ``` + * + * @param defer Indicates whether to defer location change interception. + * Passing no parameter is equivalent to `true`. + */ + UrlRouterProvider.prototype.deferIntercept = function (defer) { + this._urlRouter.deferIntercept(defer); + }; + + return UrlRouterProvider; + }()); + + /** + * # Angular 1 types + * + * UI-Router core provides various Typescript types which you can use for code completion and validating parameter values, etc. + * The customizations to the core types for Angular UI-Router are documented here. + * + * The optional [[$resolve]] service is also documented here. + * + * @module ng1 + * @preferred + */ + /** for typedoc */ + ng.module("ui.router.angular1", []); + var mod_init = ng.module('ui.router.init', []); + var mod_util = ng.module('ui.router.util', ['ng', 'ui.router.init']); + var mod_rtr = ng.module('ui.router.router', ['ui.router.util']); + var mod_state = ng.module('ui.router.state', ['ui.router.router', 'ui.router.util', 'ui.router.angular1']); + var mod_main = ng.module('ui.router', ['ui.router.init', 'ui.router.state', 'ui.router.angular1']); + var mod_cmpt = ng.module('ui.router.compat', ['ui.router']); // tslint:disable-line + var router = null; + $uiRouter.$inject = ['$locationProvider']; + /** This angular 1 provider instantiates a Router and exposes its services via the angular injector */ + function $uiRouter($locationProvider) { + // Create a new instance of the Router when the $uiRouterProvider is initialized + router = this.router = new UIRouter(); + router.stateProvider = new StateProvider(router.stateRegistry, router.stateService); + // Apply ng1 specific StateBuilder code for `views`, `resolve`, and `onExit/Retain/Enter` properties + router.stateRegistry.decorator("views", ng1ViewsBuilder); + router.stateRegistry.decorator("onExit", getStateHookBuilder("onExit")); + router.stateRegistry.decorator("onRetain", getStateHookBuilder("onRetain")); + router.stateRegistry.decorator("onEnter", getStateHookBuilder("onEnter")); + router.viewService._pluginapi._viewConfigFactory('ng1', getNg1ViewConfigFactory()); + var ng1LocationService = router.locationService = router.locationConfig = new Ng1LocationServices($locationProvider); + Ng1LocationServices.monkeyPatchPathParameterType(router); + // backwards compat: also expose router instance as $uiRouterProvider.router + router['router'] = router; + router['$get'] = $get; + $get.$inject = ['$location', '$browser', '$sniffer', '$rootScope', '$http', '$templateCache']; + function $get($location, $browser, $sniffer, $rootScope, $http, $templateCache) { + ng1LocationService._runtimeServices($rootScope, $location, $sniffer, $browser); + delete router['router']; + delete router['$get']; + return router; + } + return router; + } + var getProviderFor = function (serviceName) { return ['$uiRouterProvider', function ($urp) { + var service = $urp.router[serviceName]; + service["$get"] = function () { return service; }; + return service; + }]; }; +// This effectively calls $get() on `$uiRouterProvider` to trigger init (when ng enters runtime) + runBlock.$inject = ['$injector', '$q', '$uiRouter']; + function runBlock($injector, $q, $uiRouter) { + services.$injector = $injector; + services.$q = $q; + // The $injector is now available. + // Find any resolvables that had dependency annotation deferred + $uiRouter.stateRegistry.get() + .map(function (x) { return x.$$state().resolvables; }) + .reduce(unnestR, []) + .filter(function (x) { return x.deps === "deferred"; }) + .forEach(function (resolvable) { return resolvable.deps = $injector.annotate(resolvable.resolveFn, $injector.strictDi); }); + } +// $urlRouter service and $urlRouterProvider + var getUrlRouterProvider = function (uiRouter) { + return uiRouter.urlRouterProvider = new UrlRouterProvider(uiRouter); + }; +// $state service and $stateProvider +// $urlRouter service and $urlRouterProvider + var getStateProvider = function () { + return extend(router.stateProvider, { $get: function () { return router.stateService; } }); + }; + watchDigests.$inject = ['$rootScope']; + function watchDigests($rootScope) { + $rootScope.$watch(function () { trace.approximateDigests++; }); + } + mod_init.provider("$uiRouter", $uiRouter); + mod_rtr.provider('$urlRouter', ['$uiRouterProvider', getUrlRouterProvider]); + mod_util.provider('$urlService', getProviderFor('urlService')); + mod_util.provider('$urlMatcherFactory', ['$uiRouterProvider', function () { return router.urlMatcherFactory; }]); + mod_util.provider('$templateFactory', function () { return new TemplateFactory(); }); + mod_state.provider('$stateRegistry', getProviderFor('stateRegistry')); + mod_state.provider('$uiRouterGlobals', getProviderFor('globals')); + mod_state.provider('$transitions', getProviderFor('transitionService')); + mod_state.provider('$state', ['$uiRouterProvider', getStateProvider]); + mod_state.factory('$stateParams', ['$uiRouter', function ($uiRouter) { return $uiRouter.globals.params; }]); + mod_main.factory('$view', function () { return router.viewService; }); + mod_main.service("$trace", function () { return trace; }); + mod_main.run(watchDigests); + mod_util.run(['$urlMatcherFactory', function ($urlMatcherFactory) { }]); + mod_state.run(['$state', function ($state) { }]); + mod_rtr.run(['$urlRouter', function ($urlRouter) { }]); + mod_init.run(runBlock); + /** @hidden TODO: find a place to move this */ + var getLocals = function (ctx) { + var tokens = ctx.getTokens().filter(isString); + var tuples = tokens.map(function (key) { + var resolvable = ctx.getResolvable(key); + var waitPolicy = ctx.getPolicy(resolvable).async; + return [key, waitPolicy === 'NOWAIT' ? resolvable.promise : resolvable.data]; + }); + return tuples.reduce(applyPairs, {}); + }; + + /** + * # Angular 1 injectable services + * + * This is a list of the objects which can be injected using angular's injector. + * + * There are three different kind of injectable objects: + * + * ## **Provider** objects + * #### injectable into a `.config()` block during configtime + * + * - [[$uiRouterProvider]]: The UI-Router instance + * - [[$stateProvider]]: State registration + * - [[$transitionsProvider]]: Transition hooks + * - [[$urlServiceProvider]]: All URL related public APIs + * + * - [[$uiViewScrollProvider]]: Disable ui-router view scrolling + * - [[$urlRouterProvider]]: (deprecated) Url matching rules + * - [[$urlMatcherFactoryProvider]]: (deprecated) Url parsing config + * + * ## **Service** objects + * #### injectable globally during runtime + * + * - [[$uiRouter]]: The UI-Router instance + * - [[$trace]]: Enable transition trace/debug + * - [[$transitions]]: Transition hooks + * - [[$state]]: Imperative state related APIs + * - [[$stateRegistry]]: State registration + * - [[$urlService]]: All URL related public APIs + * - [[$uiRouterGlobals]]: Global variables + * - [[$uiViewScroll]]: Scroll an element into view + * + * - [[$stateParams]]: (deprecated) Global state param values + * - [[$urlRouter]]: (deprecated) URL synchronization + * - [[$urlMatcherFactory]]: (deprecated) URL parsing config + * + * ## **Per-Transition** objects + * + * - These kind of objects are injectable into: + * - Resolves ([[Ng1StateDeclaration.resolve]]), + * - Transition Hooks ([[TransitionService.onStart]], etc), + * - Routed Controllers ([[Ng1ViewDeclaration.controller]]) + * + * #### Different instances are injected based on the [[Transition]] + * + * - [[$transition$]]: The current Transition object + * - [[$stateParams]]: State param values for pending Transition (deprecated) + * - Any resolve data defined using [[Ng1StateDeclaration.resolve]] + * + * @ng1api + * @preferred + * @module injectables + */ /** */ + /** + * The current (or pending) State Parameters + * + * An injectable global **Service Object** which holds the state parameters for the latest **SUCCESSFUL** transition. + * + * The values are not updated until *after* a `Transition` successfully completes. + * + * **Also:** an injectable **Per-Transition Object** object which holds the pending state parameters for the pending `Transition` currently running. + * + * ### Deprecation warning: + * + * The value injected for `$stateParams` is different depending on where it is injected. + * + * - When injected into an angular service, the object injected is the global **Service Object** with the parameter values for the latest successful `Transition`. + * - When injected into transition hooks, resolves, or view controllers, the object is the **Per-Transition Object** with the parameter values for the running `Transition`. + * + * Because of these confusing details, this service is deprecated. + * + * ### Instead of using the global `$stateParams` service object, + * inject [[$uiRouterGlobals]] and use [[UIRouterGlobals.params]] + * + * ```js + * MyService.$inject = ['$uiRouterGlobals']; + * function MyService($uiRouterGlobals) { + * return { + * paramValues: function () { + * return $uiRouterGlobals.params; + * } + * } + * } + * ``` + * + * ### Instead of using the per-transition `$stateParams` object, + * inject the current `Transition` (as [[$transition$]]) and use [[Transition.params]] + * + * ```js + * MyController.$inject = ['$transition$']; + * function MyController($transition$) { + * var username = $transition$.params().username; + * // .. do something with username + * } + * ``` + * + * --- + * + * This object can be injected into other services. + * + * #### Deprecated Example: + * ```js + * SomeService.$inject = ['$http', '$stateParams']; + * function SomeService($http, $stateParams) { + * return { + * getUser: function() { + * return $http.get('/api/users/' + $stateParams.username); + * } + * } + * }; + * angular.service('SomeService', SomeService); + * ``` + * @deprecated + */ + + /** + * # Angular 1 Directives + * + * These are the directives included in UI-Router for Angular 1. + * These directives are used in templates to create viewports and link/navigate to states. + * + * @ng1api + * @preferred + * @module directives + */ /** for typedoc */ + /** @hidden */ + function parseStateRef(ref) { + var paramsOnly = ref.match(/^\s*({[^}]*})\s*$/), parsed; + if (paramsOnly) + ref = '(' + paramsOnly[1] + ')'; + parsed = ref.replace(/\n/g, " ").match(/^\s*([^(]*?)\s*(\((.*)\))?\s*$/); + if (!parsed || parsed.length !== 4) + throw new Error("Invalid state ref '" + ref + "'"); + return { state: parsed[1] || null, paramExpr: parsed[3] || null }; + } + /** @hidden */ + function stateContext(el) { + var $uiView = el.parent().inheritedData('$uiView'); + var path = parse('$cfg.path')($uiView); + return path ? tail(path).state.name : undefined; + } + /** @hidden */ + function processedDef($state, $element, def) { + var uiState = def.uiState || $state.current.name; + var uiStateOpts = extend(defaultOpts($element, $state), def.uiStateOpts || {}); + var href = $state.href(uiState, def.uiStateParams, uiStateOpts); + return { uiState: uiState, uiStateParams: def.uiStateParams, uiStateOpts: uiStateOpts, href: href }; + } + /** @hidden */ + function getTypeInfo(el) { + // SVGAElement does not use the href attribute, but rather the 'xlinkHref' attribute. + var isSvg = Object.prototype.toString.call(el.prop('href')) === '[object SVGAnimatedString]'; + var isForm = el[0].nodeName === "FORM"; + return { + attr: isForm ? "action" : (isSvg ? 'xlink:href' : 'href'), + isAnchor: el.prop("tagName").toUpperCase() === "A", + clickable: !isForm + }; + } + /** @hidden */ + function clickHook(el, $state, $timeout, type, getDef) { + return function (e) { + var button = e.which || e.button, target = getDef(); + if (!(button > 1 || e.ctrlKey || e.metaKey || e.shiftKey || el.attr('target'))) { + // HACK: This is to allow ng-clicks to be processed before the transition is initiated: + var transition = $timeout(function () { + $state.go(target.uiState, target.uiStateParams, target.uiStateOpts); + }); + e.preventDefault(); + // if the state has no URL, ignore one preventDefault from the directive. + var ignorePreventDefaultCount = type.isAnchor && !target.href ? 1 : 0; + e.preventDefault = function () { + if (ignorePreventDefaultCount-- <= 0) + $timeout.cancel(transition); + }; + } + }; + } + /** @hidden */ + function defaultOpts(el, $state) { + return { + relative: stateContext(el) || $state.$current, + inherit: true, + source: "sref" + }; + } + /** @hidden */ + function bindEvents(element, scope, hookFn, uiStateOpts) { + var events; + if (uiStateOpts) { + events = uiStateOpts.events; + } + if (!isArray(events)) { + events = ['click']; + } + var on = element.on ? 'on' : 'bind'; + for (var _i = 0, events_1 = events; _i < events_1.length; _i++) { + var event_1 = events_1[_i]; + element[on](event_1, hookFn); + } + scope.$on('$destroy', function () { + var off = element.off ? 'off' : 'unbind'; + for (var _i = 0, events_2 = events; _i < events_2.length; _i++) { + var event_2 = events_2[_i]; + element[off](event_2, hookFn); + } + }); + } + /** + * `ui-sref`: A directive for linking to a state + * + * A directive which links to a state (and optionally, parameters). + * When clicked, this directive activates the linked state with the supplied parameter values. + * + * ### Linked State + * The attribute value of the `ui-sref` is the name of the state to link to. + * + * #### Example: + * This will activate the `home` state when the link is clicked. + * ```html + * Home + * ``` + * + * ### Relative Links + * You can also use relative state paths within `ui-sref`, just like a relative path passed to `$state.go()` ([[StateService.go]]). + * You just need to be aware that the path is relative to the state that *created* the link. + * This allows a state to create a relative `ui-sref` which always targets the same destination. + * + * #### Example: + * Both these links are relative to the parent state, even when a child state is currently active. + * ```html + * child 1 state + * child 2 state + * ``` + * + * This link activates the parent state. + * ```html + * Return + * ``` + * + * ### hrefs + * If the linked state has a URL, the directive will automatically generate and + * update the `href` attribute (using the [[StateService.href]] method). + * + * #### Example: + * Assuming the `users` state has a url of `/users/` + * ```html + * Users + * ``` + * + * ### Parameter Values + * In addition to the state name, a `ui-sref` can include parameter values which are applied when activating the state. + * Param values can be provided in the `ui-sref` value after the state name, enclosed by parentheses. + * The content inside the parentheses is an expression, evaluated to the parameter values. + * + * #### Example: + * This example renders a list of links to users. + * The state's `userId` parameter value comes from each user's `user.id` property. + * ```html + *
        • + * {{ user.displayName }} + *
        • + * ``` + * + * Note: + * The parameter values expression is `$watch`ed for updates. + * + * ### Transition Options + * You can specify [[TransitionOptions]] to pass to [[StateService.go]] by using the `ui-sref-opts` attribute. + * Options are restricted to `location`, `inherit`, and `reload`. + * + * #### Example: + * ```html + * Home + * ``` + * + * ### Other DOM Events + * + * You can also customize which DOM events to respond to (instead of `click`) by + * providing an `events` array in the `ui-sref-opts` attribute. + * + * #### Example: + * ```html + * + * ``` + * + * ### Highlighting the active link + * This directive can be used in conjunction with [[uiSrefActive]] to highlight the active link. + * + * ### Examples + * If you have the following template: + * + * ```html + * Home + * About + * Next page + * + * + * ``` + * + * Then (assuming the current state is `contacts`) the rendered html including hrefs would be: + * + * ```html + * Home + * About + * Next page + * + *
            + *
          • + * Joe + *
          • + *
          • + * Alice + *
          • + *
          • + * Bob + *
          • + *
          + * + * Home + * ``` + * + * ### Notes + * + * - You can use `ui-sref` to change **only the parameter values** by omitting the state name and parentheses. + * #### Example: + * Sets the `lang` parameter to `en` and remains on the same state. + * + * ```html + * English + * ``` + * + * - A middle-click, right-click, or ctrl-click is handled (natively) by the browser to open the href in a new window, for example. + * + * - Unlike the parameter values expression, the state name is not `$watch`ed (for performance reasons). + * If you need to dynamically update the state being linked to, use the fully dynamic [[uiState]] directive. + */ + var uiSref; + uiSref = ['$uiRouter', '$timeout', + function $StateRefDirective($uiRouter, $timeout) { + var $state = $uiRouter.stateService; + return { + restrict: 'A', + require: ['?^uiSrefActive', '?^uiSrefActiveEq'], + link: function (scope, element, attrs, uiSrefActive) { + var type = getTypeInfo(element); + var active = uiSrefActive[1] || uiSrefActive[0]; + var unlinkInfoFn = null; + var hookFn; + var rawDef = {}; + var getDef = function () { return processedDef($state, element, rawDef); }; + var ref = parseStateRef(attrs.uiSref); + rawDef.uiState = ref.state; + rawDef.uiStateOpts = attrs.uiSrefOpts ? scope.$eval(attrs.uiSrefOpts) : {}; + function update() { + var def = getDef(); + if (unlinkInfoFn) + unlinkInfoFn(); + if (active) + unlinkInfoFn = active.$$addStateInfo(def.uiState, def.uiStateParams); + if (def.href != null) + attrs.$set(type.attr, def.href); + } + if (ref.paramExpr) { + scope.$watch(ref.paramExpr, function (val) { + rawDef.uiStateParams = extend({}, val); + update(); + }, true); + rawDef.uiStateParams = extend({}, scope.$eval(ref.paramExpr)); + } + update(); + scope.$on('$destroy', $uiRouter.stateRegistry.onStatesChanged(update)); + scope.$on('$destroy', $uiRouter.transitionService.onSuccess({}, update)); + if (!type.clickable) + return; + hookFn = clickHook(element, $state, $timeout, type, getDef); + bindEvents(element, scope, hookFn, rawDef.uiStateOpts); + } + }; + }]; + /** + * `ui-state`: A fully dynamic directive for linking to a state + * + * A directive which links to a state (and optionally, parameters). + * When clicked, this directive activates the linked state with the supplied parameter values. + * + * **This directive is very similar to [[uiSref]], but it `$observe`s and `$watch`es/evaluates all its inputs.** + * + * A directive which links to a state (and optionally, parameters). + * When clicked, this directive activates the linked state with the supplied parameter values. + * + * ### Linked State + * The attribute value of `ui-state` is an expression which is `$watch`ed and evaluated as the state to link to. + * **This is in contrast with `ui-sref`, which takes a state name as a string literal.** + * + * #### Example: + * Create a list of links. + * ```html + *
        • + * {{ link.displayName }} + *
        • + * ``` + * + * ### Relative Links + * If the expression evaluates to a relative path, it is processed like [[uiSref]]. + * You just need to be aware that the path is relative to the state that *created* the link. + * This allows a state to create relative `ui-state` which always targets the same destination. + * + * ### hrefs + * If the linked state has a URL, the directive will automatically generate and + * update the `href` attribute (using the [[StateService.href]] method). + * + * ### Parameter Values + * In addition to the state name expression, a `ui-state` can include parameter values which are applied when activating the state. + * Param values should be provided using the `ui-state-params` attribute. + * The `ui-state-params` attribute value is `$watch`ed and evaluated as an expression. + * + * #### Example: + * This example renders a list of links with param values. + * The state's `userId` parameter value comes from each user's `user.id` property. + * ```html + *
        • + * {{ link.displayName }} + *
        • + * ``` + * + * ### Transition Options + * You can specify [[TransitionOptions]] to pass to [[StateService.go]] by using the `ui-state-opts` attribute. + * Options are restricted to `location`, `inherit`, and `reload`. + * The value of the `ui-state-opts` is `$watch`ed and evaluated as an expression. + * + * #### Example: + * ```html + * Home + * ``` + * + * ### Other DOM Events + * + * You can also customize which DOM events to respond to (instead of `click`) by + * providing an `events` array in the `ui-state-opts` attribute. + * + * #### Example: + * ```html + * + * ``` + * + * ### Highlighting the active link + * This directive can be used in conjunction with [[uiSrefActive]] to highlight the active link. + * + * ### Notes + * + * - You can use `ui-params` to change **only the parameter values** by omitting the state name and supplying only `ui-state-params`. + * However, it might be simpler to use [[uiSref]] parameter-only links. + * + * #### Example: + * Sets the `lang` parameter to `en` and remains on the same state. + * + * ```html + * English + * ``` + * + * - A middle-click, right-click, or ctrl-click is handled (natively) by the browser to open the href in a new window, for example. + * ``` + */ + var uiState; + uiState = ['$uiRouter', '$timeout', + function $StateRefDynamicDirective($uiRouter, $timeout) { + var $state = $uiRouter.stateService; + return { + restrict: 'A', + require: ['?^uiSrefActive', '?^uiSrefActiveEq'], + link: function (scope, element, attrs, uiSrefActive) { + var type = getTypeInfo(element); + var active = uiSrefActive[1] || uiSrefActive[0]; + var unlinkInfoFn = null; + var hookFn; + var rawDef = {}; + var getDef = function () { return processedDef($state, element, rawDef); }; + var inputAttrs = ['uiState', 'uiStateParams', 'uiStateOpts']; + var watchDeregFns = inputAttrs.reduce(function (acc, attr) { return (acc[attr] = noop$1, acc); }, {}); + function update() { + var def = getDef(); + if (unlinkInfoFn) + unlinkInfoFn(); + if (active) + unlinkInfoFn = active.$$addStateInfo(def.uiState, def.uiStateParams); + if (def.href != null) + attrs.$set(type.attr, def.href); + } + inputAttrs.forEach(function (field) { + rawDef[field] = attrs[field] ? scope.$eval(attrs[field]) : null; + attrs.$observe(field, function (expr) { + watchDeregFns[field](); + watchDeregFns[field] = scope.$watch(expr, function (newval) { + rawDef[field] = newval; + update(); + }, true); + }); + }); + update(); + scope.$on('$destroy', $uiRouter.stateRegistry.onStatesChanged(update)); + scope.$on('$destroy', $uiRouter.transitionService.onSuccess({}, update)); + if (!type.clickable) + return; + hookFn = clickHook(element, $state, $timeout, type, getDef); + bindEvents(element, scope, hookFn, rawDef.uiStateOpts); + } + }; + }]; + /** + * `ui-sref-active` and `ui-sref-active-eq`: A directive that adds a CSS class when a `ui-sref` is active + * + * A directive working alongside [[uiSref]] and [[uiState]] to add classes to an element when the + * related directive's state is active (and remove them when it is inactive). + * + * The primary use-case is to highlight the active link in navigation menus, + * distinguishing it from the inactive menu items. + * + * ### Linking to a `ui-sref` or `ui-state` + * `ui-sref-active` can live on the same element as `ui-sref`/`ui-state`, or it can be on a parent element. + * If a `ui-sref-active` is a parent to more than one `ui-sref`/`ui-state`, it will apply the CSS class when **any of the links are active**. + * + * ### Matching + * + * The `ui-sref-active` directive applies the CSS class when the `ui-sref`/`ui-state`'s target state **or any child state is active**. + * This is a "fuzzy match" which uses [[StateService.includes]]. + * + * The `ui-sref-active-eq` directive applies the CSS class when the `ui-sref`/`ui-state`'s target state is directly active (not when child states are active). + * This is an "exact match" which uses [[StateService.is]]. + * + * ### Parameter values + * If the `ui-sref`/`ui-state` includes parameter values, the current parameter values must match the link's values for the link to be highlighted. + * This allows a list of links to the same state with different parameters to be rendered, and the correct one highlighted. + * + * #### Example: + * ```html + *
        • + * {{ user.lastName }} + *
        • + * ``` + * + * ### Examples + * + * Given the following template: + * #### Example: + * ```html + * + * ``` + * + * When the app state is `app.user` (or any child state), + * and contains the state parameter "user" with value "bilbobaggins", + * the resulting HTML will appear as (note the 'active' class): + * + * ```html + * + * ``` + * + * ### Glob mode + * + * It is possible to pass `ui-sref-active` an expression that evaluates to an object. + * The objects keys represent active class names and values represent the respective state names/globs. + * `ui-sref-active` will match if the current active state **includes** any of + * the specified state names/globs, even the abstract ones. + * + * #### Example: + * Given the following template, with "admin" being an abstract state: + * ```html + *
          + * Roles + *
          + * ``` + * + * When the current state is "admin.roles" the "active" class will be applied to both the
          and elements. + * It is important to note that the state names/globs passed to `ui-sref-active` override any state provided by a linked `ui-sref`. + * + * ### Notes: + * + * - The class name is interpolated **once** during the directives link time (any further changes to the + * interpolated value are ignored). + * + * - Multiple classes may be specified in a space-separated format: `ui-sref-active='class1 class2 class3'` + */ + var uiSrefActive; + uiSrefActive = ['$state', '$stateParams', '$interpolate', '$uiRouter', + function $StateRefActiveDirective($state, $stateParams, $interpolate, $uiRouter) { + return { + restrict: "A", + controller: ['$scope', '$element', '$attrs', + function ($scope, $element, $attrs) { + var states = [], activeEqClass, uiSrefActive; + // There probably isn't much point in $observing this + // uiSrefActive and uiSrefActiveEq share the same directive object with some + // slight difference in logic routing + activeEqClass = $interpolate($attrs.uiSrefActiveEq || '', false)($scope); + try { + uiSrefActive = $scope.$eval($attrs.uiSrefActive); + } + catch (e) { + // Do nothing. uiSrefActive is not a valid expression. + // Fall back to using $interpolate below + } + uiSrefActive = uiSrefActive || $interpolate($attrs.uiSrefActive || '', false)($scope); + if (isObject(uiSrefActive)) { + forEach(uiSrefActive, function (stateOrName, activeClass) { + if (isString(stateOrName)) { + var ref = parseStateRef(stateOrName); + addState(ref.state, $scope.$eval(ref.paramExpr), activeClass); + } + }); + } + // Allow uiSref to communicate with uiSrefActive[Equals] + this.$$addStateInfo = function (newState, newParams) { + // we already got an explicit state provided by ui-sref-active, so we + // shadow the one that comes from ui-sref + if (isObject(uiSrefActive) && states.length > 0) { + return; + } + var deregister = addState(newState, newParams, uiSrefActive); + update(); + return deregister; + }; + function updateAfterTransition(trans) { + trans.promise.then(update, noop$1); + } + $scope.$on('$stateChangeSuccess', update); + $scope.$on('$destroy', $uiRouter.transitionService.onStart({}, updateAfterTransition)); + if ($uiRouter.globals.transition) { + updateAfterTransition($uiRouter.globals.transition); + } + function addState(stateName, stateParams, activeClass) { + var state = $state.get(stateName, stateContext($element)); + var stateInfo = { + state: state || { name: stateName }, + params: stateParams, + activeClass: activeClass + }; + states.push(stateInfo); + return function removeState() { + removeFrom(states)(stateInfo); + }; + } + // Update route state + function update() { + var splitClasses = function (str) { + return str.split(/\s/).filter(identity); + }; + var getClasses = function (stateList) { + return stateList.map(function (x) { return x.activeClass; }).map(splitClasses).reduce(unnestR, []); + }; + var allClasses = getClasses(states).concat(splitClasses(activeEqClass)).reduce(uniqR, []); + var fuzzyClasses = getClasses(states.filter(function (x) { return $state.includes(x.state.name, x.params); })); + var exactlyMatchesAny = !!states.filter(function (x) { return $state.is(x.state.name, x.params); }).length; + var exactClasses = exactlyMatchesAny ? splitClasses(activeEqClass) : []; + var addClasses = fuzzyClasses.concat(exactClasses).reduce(uniqR, []); + var removeClasses = allClasses.filter(function (cls) { return !inArray(addClasses, cls); }); + $scope.$evalAsync(function () { + addClasses.forEach(function (className) { return $element.addClass(className); }); + removeClasses.forEach(function (className) { return $element.removeClass(className); }); + }); + } + update(); + }] + }; + }]; + ng.module('ui.router.state') + .directive('uiSref', uiSref) + .directive('uiSrefActive', uiSrefActive) + .directive('uiSrefActiveEq', uiSrefActive) + .directive('uiState', uiState); + + /** @module ng1 */ /** for typedoc */ + /** + * `isState` Filter: truthy if the current state is the parameter + * + * Translates to [[StateService.is]] `$state.is("stateName")`. + * + * #### Example: + * ```html + *
          show if state is 'stateName'
          + * ``` + */ + $IsStateFilter.$inject = ['$state']; + function $IsStateFilter($state) { + var isFilter = function (state, params, options) { + return $state.is(state, params, options); + }; + isFilter.$stateful = true; + return isFilter; + } + /** + * `includedByState` Filter: truthy if the current state includes the parameter + * + * Translates to [[StateService.includes]]` $state.is("fullOrPartialStateName")`. + * + * #### Example: + * ```html + *
          show if state includes 'fullOrPartialStateName'
          + * ``` + */ + $IncludedByStateFilter.$inject = ['$state']; + function $IncludedByStateFilter($state) { + var includesFilter = function (state, params, options) { + return $state.includes(state, params, options); + }; + includesFilter.$stateful = true; + return includesFilter; + } + ng.module('ui.router.state') + .filter('isState', $IsStateFilter) + .filter('includedByState', $IncludedByStateFilter); + + /** + * @ng1api + * @module directives + */ /** for typedoc */ + /** + * `ui-view`: A viewport directive which is filled in by a view from the active state. + * + * ### Attributes + * + * - `name`: (Optional) A view name. + * The name should be unique amongst the other views in the same state. + * You can have views of the same name that live in different states. + * The ui-view can be targeted in a View using the name ([[Ng1StateDeclaration.views]]). + * + * - `autoscroll`: an expression. When it evaluates to true, the `ui-view` will be scrolled into view when it is activated. + * Uses [[$uiViewScroll]] to do the scrolling. + * + * - `onload`: Expression to evaluate whenever the view updates. + * + * #### Example: + * A view can be unnamed or named. + * ```html + * + *
          + * + * + *
          + * + * + * + * ``` + * + * You can only have one unnamed view within any template (or root html). If you are only using a + * single view and it is unnamed then you can populate it like so: + * + * ```html + *
          + * $stateProvider.state("home", { + * template: "

          HELLO!

          " + * }) + * ``` + * + * The above is a convenient shortcut equivalent to specifying your view explicitly with the + * [[Ng1StateDeclaration.views]] config property, by name, in this case an empty name: + * + * ```js + * $stateProvider.state("home", { + * views: { + * "": { + * template: "

          HELLO!

          " + * } + * } + * }) + * ``` + * + * But typically you'll only use the views property if you name your view or have more than one view + * in the same template. There's not really a compelling reason to name a view if its the only one, + * but you could if you wanted, like so: + * + * ```html + *
          + * ``` + * + * ```js + * $stateProvider.state("home", { + * views: { + * "main": { + * template: "

          HELLO!

          " + * } + * } + * }) + * ``` + * + * Really though, you'll use views to set up multiple views: + * + * ```html + *
          + *
          + *
          + * ``` + * + * ```js + * $stateProvider.state("home", { + * views: { + * "": { + * template: "

          HELLO!

          " + * }, + * "chart": { + * template: "" + * }, + * "data": { + * template: "" + * } + * } + * }) + * ``` + * + * #### Examples for `autoscroll`: + * ```html + * + * + * + * + * + * + * + * ``` + * + * Resolve data: + * + * The resolved data from the state's `resolve` block is placed on the scope as `$resolve` (this + * can be customized using [[Ng1ViewDeclaration.resolveAs]]). This can be then accessed from the template. + * + * Note that when `controllerAs` is being used, `$resolve` is set on the controller instance *after* the + * controller is instantiated. The `$onInit()` hook can be used to perform initialization code which + * depends on `$resolve` data. + * + * #### Example: + * ```js + * $stateProvider.state('home', { + * template: '', + * resolve: { + * user: function(UserService) { return UserService.fetchUser(); } + * } + * }); + * ``` + */ + var uiView; + uiView = ['$view', '$animate', '$uiViewScroll', '$interpolate', '$q', + function $ViewDirective($view, $animate, $uiViewScroll, $interpolate, $q) { + function getRenderer(attrs, scope) { + return { + enter: function (element, target, cb) { + if (ng.version.minor > 2) { + $animate.enter(element, null, target).then(cb); + } + else { + $animate.enter(element, null, target, cb); + } + }, + leave: function (element, cb) { + if (ng.version.minor > 2) { + $animate.leave(element).then(cb); + } + else { + $animate.leave(element, cb); + } + } + }; + } + function configsEqual(config1, config2) { + return config1 === config2; + } + var rootData = { + $cfg: { viewDecl: { $context: $view._pluginapi._rootViewContext() } }, + $uiView: {} + }; + var directive = { + count: 0, + restrict: 'ECA', + terminal: true, + priority: 400, + transclude: 'element', + compile: function (tElement, tAttrs, $transclude) { + return function (scope, $element, attrs) { + var previousEl, currentEl, currentScope, unregister, onloadExp = attrs['onload'] || '', autoScrollExp = attrs['autoscroll'], renderer = getRenderer(attrs, scope), viewConfig = undefined, inherited = $element.inheritedData('$uiView') || rootData, name = $interpolate(attrs['uiView'] || attrs['name'] || '')(scope) || '$default'; + var activeUIView = { + $type: 'ng1', + id: directive.count++, + name: name, + fqn: inherited.$uiView.fqn ? inherited.$uiView.fqn + "." + name : name, + config: null, + configUpdated: configUpdatedCallback, + get creationContext() { + var fromParentTagConfig = parse('$cfg.viewDecl.$context')(inherited); + // Allow + // See https://github.com/angular-ui/ui-router/issues/3355 + var fromParentTag = parse('$uiView.creationContext')(inherited); + return fromParentTagConfig || fromParentTag; + } + }; + trace.traceUIViewEvent("Linking", activeUIView); + function configUpdatedCallback(config) { + if (config && !(config instanceof Ng1ViewConfig)) + return; + if (configsEqual(viewConfig, config)) + return; + trace.traceUIViewConfigUpdated(activeUIView, config && config.viewDecl && config.viewDecl.$context); + viewConfig = config; + updateView(config); + } + $element.data('$uiView', { $uiView: activeUIView }); + updateView(); + unregister = $view.registerUIView(activeUIView); + scope.$on("$destroy", function () { + trace.traceUIViewEvent("Destroying/Unregistering", activeUIView); + unregister(); + }); + function cleanupLastView() { + if (previousEl) { + trace.traceUIViewEvent("Removing (previous) el", previousEl.data('$uiView')); + previousEl.remove(); + previousEl = null; + } + if (currentScope) { + trace.traceUIViewEvent("Destroying scope", activeUIView); + currentScope.$destroy(); + currentScope = null; + } + if (currentEl) { + var _viewData_1 = currentEl.data('$uiViewAnim'); + trace.traceUIViewEvent("Animate out", _viewData_1); + renderer.leave(currentEl, function () { + _viewData_1.$$animLeave.resolve(); + previousEl = null; + }); + previousEl = currentEl; + currentEl = null; + } + } + function updateView(config) { + var newScope = scope.$new(); + var animEnter = $q.defer(), animLeave = $q.defer(); + var $uiViewData = { + $cfg: config, + $uiView: activeUIView, + }; + var $uiViewAnim = { + $animEnter: animEnter.promise, + $animLeave: animLeave.promise, + $$animLeave: animLeave + }; + /** + * @ngdoc event + * @name ui.router.state.directive:ui-view#$viewContentLoading + * @eventOf ui.router.state.directive:ui-view + * @eventType emits on ui-view directive scope + * @description + * + * Fired once the view **begins loading**, *before* the DOM is rendered. + * + * @param {Object} event Event object. + * @param {string} viewName Name of the view. + */ + newScope.$emit('$viewContentLoading', name); + var cloned = $transclude(newScope, function (clone) { + clone.data('$uiViewAnim', $uiViewAnim); + clone.data('$uiView', $uiViewData); + renderer.enter(clone, $element, function onUIViewEnter() { + animEnter.resolve(); + if (currentScope) + currentScope.$emit('$viewContentAnimationEnded'); + if (isDefined(autoScrollExp) && !autoScrollExp || scope.$eval(autoScrollExp)) { + $uiViewScroll(clone); + } + }); + cleanupLastView(); + }); + currentEl = cloned; + currentScope = newScope; + /** + * @ngdoc event + * @name ui.router.state.directive:ui-view#$viewContentLoaded + * @eventOf ui.router.state.directive:ui-view + * @eventType emits on ui-view directive scope + * @description * + * Fired once the view is **loaded**, *after* the DOM is rendered. + * + * @param {Object} event Event object. + */ + currentScope.$emit('$viewContentLoaded', config || viewConfig); + currentScope.$eval(onloadExp); + } + }; + } + }; + return directive; + }]; + $ViewDirectiveFill.$inject = ['$compile', '$controller', '$transitions', '$view', '$q', '$timeout']; + /** @hidden */ + function $ViewDirectiveFill($compile, $controller, $transitions, $view, $q, $timeout) { + var getControllerAs = parse('viewDecl.controllerAs'); + var getResolveAs = parse('viewDecl.resolveAs'); + return { + restrict: 'ECA', + priority: -400, + compile: function (tElement) { + var initial = tElement.html(); + tElement.empty(); + return function (scope, $element) { + var data = $element.data('$uiView'); + if (!data) { + $element.html(initial); + $compile($element.contents())(scope); + return; + } + var cfg = data.$cfg || { viewDecl: {}, getTemplate: ng_from_import.noop }; + var resolveCtx = cfg.path && new ResolveContext(cfg.path); + $element.html(cfg.getTemplate($element, resolveCtx) || initial); + trace.traceUIViewFill(data.$uiView, $element.html()); + var link = $compile($element.contents()); + var controller = cfg.controller; + var controllerAs = getControllerAs(cfg); + var resolveAs = getResolveAs(cfg); + var locals = resolveCtx && getLocals(resolveCtx); + scope[resolveAs] = locals; + if (controller) { + var controllerInstance = $controller(controller, extend({}, locals, { $scope: scope, $element: $element })); + if (controllerAs) { + scope[controllerAs] = controllerInstance; + scope[controllerAs][resolveAs] = locals; + } + // TODO: Use $view service as a central point for registering component-level hooks + // Then, when a component is created, tell the $view service, so it can invoke hooks + // $view.componentLoaded(controllerInstance, { $scope: scope, $element: $element }); + // scope.$on('$destroy', () => $view.componentUnloaded(controllerInstance, { $scope: scope, $element: $element })); + $element.data('$ngControllerController', controllerInstance); + $element.children().data('$ngControllerController', controllerInstance); + registerControllerCallbacks($q, $transitions, controllerInstance, scope, cfg); + } + // Wait for the component to appear in the DOM + if (isString(cfg.viewDecl.component)) { + var cmp_1 = cfg.viewDecl.component; + var kebobName = kebobString(cmp_1); + var tagRegexp_1 = new RegExp("^(x-|data-)?" + kebobName + "$", "i"); + var getComponentController = function () { + var directiveEl = [].slice.call($element[0].children) + .filter(function (el) { return el && el.tagName && tagRegexp_1.exec(el.tagName); }); + return directiveEl && ng.element(directiveEl).data("$" + cmp_1 + "Controller"); + }; + var deregisterWatch_1 = scope.$watch(getComponentController, function (ctrlInstance) { + if (!ctrlInstance) + return; + registerControllerCallbacks($q, $transitions, ctrlInstance, scope, cfg); + deregisterWatch_1(); + }); + } + link(scope); + }; + } + }; + } + /** @hidden */ + var hasComponentImpl = typeof ng.module('ui.router')['component'] === 'function'; + /** @hidden incrementing id */ + var _uiCanExitId = 0; + /** @hidden TODO: move these callbacks to $view and/or `/hooks/components.ts` or something */ + function registerControllerCallbacks($q, $transitions, controllerInstance, $scope, cfg) { + // Call $onInit() ASAP + if (isFunction(controllerInstance.$onInit) && !(cfg.viewDecl.component && hasComponentImpl)) { + controllerInstance.$onInit(); + } + var viewState = tail(cfg.path).state.self; + var hookOptions = { bind: controllerInstance }; + // Add component-level hook for onParamsChange + if (isFunction(controllerInstance.uiOnParamsChanged)) { + var resolveContext = new ResolveContext(cfg.path); + var viewCreationTrans_1 = resolveContext.getResolvable('$transition$').data; + // Fire callback on any successful transition + var paramsUpdated = function ($transition$) { + // Exit early if the $transition$ is the same as the view was created within. + // Exit early if the $transition$ will exit the state the view is for. + if ($transition$ === viewCreationTrans_1 || $transition$.exiting().indexOf(viewState) !== -1) + return; + var toParams = $transition$.params("to"); + var fromParams = $transition$.params("from"); + var toSchema = $transition$.treeChanges().to.map(function (node) { return node.paramSchema; }).reduce(unnestR, []); + var fromSchema = $transition$.treeChanges().from.map(function (node) { return node.paramSchema; }).reduce(unnestR, []); + // Find the to params that have different values than the from params + var changedToParams = toSchema.filter(function (param) { + var idx = fromSchema.indexOf(param); + return idx === -1 || !fromSchema[idx].type.equals(toParams[param.id], fromParams[param.id]); + }); + // Only trigger callback if a to param has changed or is new + if (changedToParams.length) { + var changedKeys_1 = changedToParams.map(function (x) { return x.id; }); + // Filter the params to only changed/new to params. `$transition$.params()` may be used to get all params. + var newValues = filter(toParams, function (val, key) { return changedKeys_1.indexOf(key) !== -1; }); + controllerInstance.uiOnParamsChanged(newValues, $transition$); + } + }; + $scope.$on('$destroy', $transitions.onSuccess({}, paramsUpdated, hookOptions)); + } + // Add component-level hook for uiCanExit + if (isFunction(controllerInstance.uiCanExit)) { + var id_1 = _uiCanExitId++; + var cacheProp_1 = '_uiCanExitIds'; + // Returns true if a redirect transition already answered truthy + var prevTruthyAnswer_1 = function (trans) { + return !!trans && (trans[cacheProp_1] && trans[cacheProp_1][id_1] === true || prevTruthyAnswer_1(trans.redirectedFrom())); + }; + // If a user answered yes, but the transition was later redirected, don't also ask for the new redirect transition + var wrappedHook = function (trans) { + var promise, ids = trans[cacheProp_1] = trans[cacheProp_1] || {}; + if (!prevTruthyAnswer_1(trans)) { + promise = $q.when(controllerInstance.uiCanExit(trans)); + promise.then(function (val) { return ids[id_1] = (val !== false); }); + } + return promise; + }; + var criteria = { exiting: viewState.name }; + $scope.$on('$destroy', $transitions.onBefore(criteria, wrappedHook, hookOptions)); + } + } + ng.module('ui.router.state').directive('uiView', uiView); + ng.module('ui.router.state').directive('uiView', $ViewDirectiveFill); + + /** @module ng1 */ /** */ + /** @hidden */ + function $ViewScrollProvider() { + var useAnchorScroll = false; + this.useAnchorScroll = function () { + useAnchorScroll = true; + }; + this.$get = ['$anchorScroll', '$timeout', function ($anchorScroll, $timeout) { + if (useAnchorScroll) { + return $anchorScroll; + } + return function ($element) { + return $timeout(function () { + $element[0].scrollIntoView(); + }, 0, false); + }; + }]; + } + ng.module('ui.router.state').provider('$uiViewScroll', $ViewScrollProvider); + + /** + * Main entry point for angular 1.x build + * @module ng1 + */ /** */ + var index = "ui.router"; + + exports['default'] = index; + exports.core = index$1; + exports.watchDigests = watchDigests; + exports.getLocals = getLocals; + exports.getNg1ViewConfigFactory = getNg1ViewConfigFactory; + exports.ng1ViewsBuilder = ng1ViewsBuilder; + exports.Ng1ViewConfig = Ng1ViewConfig; + exports.StateProvider = StateProvider; + exports.UrlRouterProvider = UrlRouterProvider; + exports.root = root; + exports.fromJson = fromJson; + exports.toJson = toJson; + exports.forEach = forEach; + exports.extend = extend; + exports.equals = equals; + exports.identity = identity; + exports.noop = noop$1; + exports.createProxyFunctions = createProxyFunctions; + exports.inherit = inherit; + exports.inArray = inArray; + exports._inArray = _inArray; + exports.removeFrom = removeFrom; + exports._removeFrom = _removeFrom; + exports.pushTo = pushTo; + exports._pushTo = _pushTo; + exports.deregAll = deregAll; + exports.defaults = defaults; + exports.mergeR = mergeR; + exports.ancestors = ancestors; + exports.pick = pick; + exports.omit = omit; + exports.pluck = pluck; + exports.filter = filter; + exports.find = find; + exports.mapObj = mapObj; + exports.map = map; + exports.values = values; + exports.allTrueR = allTrueR; + exports.anyTrueR = anyTrueR; + exports.unnestR = unnestR; + exports.flattenR = flattenR; + exports.pushR = pushR; + exports.uniqR = uniqR; + exports.unnest = unnest; + exports.flatten = flatten; + exports.assertPredicate = assertPredicate; + exports.assertMap = assertMap; + exports.assertFn = assertFn; + exports.pairs = pairs; + exports.arrayTuples = arrayTuples; + exports.applyPairs = applyPairs; + exports.tail = tail; + exports.copy = copy; + exports._extend = _extend; + exports.silenceUncaughtInPromise = silenceUncaughtInPromise; + exports.silentRejection = silentRejection; + exports.notImplemented = notImplemented; + exports.services = services; + exports.Glob = Glob; + exports.curry = curry; + exports.compose = compose; + exports.pipe = pipe; + exports.prop = prop; + exports.propEq = propEq; + exports.parse = parse; + exports.not = not; + exports.and = and; + exports.or = or; + exports.all = all; + exports.any = any; + exports.is = is; + exports.eq = eq; + exports.val = val; + exports.invoke = invoke; + exports.pattern = pattern; + exports.isUndefined = isUndefined; + exports.isDefined = isDefined; + exports.isNull = isNull; + exports.isNullOrUndefined = isNullOrUndefined; + exports.isFunction = isFunction; + exports.isNumber = isNumber; + exports.isString = isString; + exports.isObject = isObject; + exports.isArray = isArray; + exports.isDate = isDate; + exports.isRegExp = isRegExp; + exports.isState = isState; + exports.isInjectable = isInjectable; + exports.isPromise = isPromise; + exports.Queue = Queue; + exports.maxLength = maxLength; + exports.padString = padString; + exports.kebobString = kebobString; + exports.functionToString = functionToString; + exports.fnToString = fnToString; + exports.stringify = stringify; + exports.beforeAfterSubstr = beforeAfterSubstr; + exports.hostRegex = hostRegex; + exports.stripFile = stripFile; + exports.splitHash = splitHash; + exports.splitQuery = splitQuery; + exports.splitEqual = splitEqual; + exports.trimHashVal = trimHashVal; + exports.splitOnDelim = splitOnDelim; + exports.joinNeighborsR = joinNeighborsR; + exports.Trace = Trace; + exports.trace = trace; + exports.Param = Param; + exports.ParamTypes = ParamTypes; + exports.StateParams = StateParams; + exports.ParamType = ParamType; + exports.PathNode = PathNode; + exports.PathUtils = PathUtils; + exports.resolvePolicies = resolvePolicies; + exports.defaultResolvePolicy = defaultResolvePolicy; + exports.Resolvable = Resolvable; + exports.NATIVE_INJECTOR_TOKEN = NATIVE_INJECTOR_TOKEN; + exports.ResolveContext = ResolveContext; + exports.resolvablesBuilder = resolvablesBuilder; + exports.StateBuilder = StateBuilder; + exports.StateObject = StateObject; + exports.StateMatcher = StateMatcher; + exports.StateQueueManager = StateQueueManager; + exports.StateRegistry = StateRegistry; + exports.StateService = StateService; + exports.TargetState = TargetState; + exports.HookBuilder = HookBuilder; + exports.matchState = matchState; + exports.RegisteredHook = RegisteredHook; + exports.makeEvent = makeEvent; + exports.Rejection = Rejection; + exports.Transition = Transition; + exports.TransitionHook = TransitionHook; + exports.TransitionEventType = TransitionEventType; + exports.defaultTransOpts = defaultTransOpts; + exports.TransitionService = TransitionService; + exports.UrlMatcher = UrlMatcher; + exports.UrlMatcherFactory = UrlMatcherFactory; + exports.UrlRouter = UrlRouter; + exports.UrlRuleFactory = UrlRuleFactory; + exports.BaseUrlRule = BaseUrlRule; + exports.UrlService = UrlService; + exports.ViewService = ViewService; + exports.UIRouterGlobals = UIRouterGlobals; + exports.UIRouter = UIRouter; + exports.$q = $q; + exports.$injector = $injector; + exports.BaseLocationServices = BaseLocationServices; + exports.HashLocationService = HashLocationService; + exports.MemoryLocationService = MemoryLocationService; + exports.PushStateLocationService = PushStateLocationService; + exports.MemoryLocationConfig = MemoryLocationConfig; + exports.BrowserLocationConfig = BrowserLocationConfig; + exports.keyValsToObjectR = keyValsToObjectR; + exports.getParams = getParams; + exports.parseUrl = parseUrl$1; + exports.buildUrl = buildUrl; + exports.locationPluginFactory = locationPluginFactory; + exports.servicesPlugin = servicesPlugin; + exports.hashLocationPlugin = hashLocationPlugin; + exports.pushStateLocationPlugin = pushStateLocationPlugin; + exports.memoryLocationPlugin = memoryLocationPlugin; + exports.UIRouterPluginBase = UIRouterPluginBase; + + Object.defineProperty(exports, '__esModule', { value: true }); + +}))); \ No newline at end of file diff --git a/docs-web/src/main/webapp/src/lib/angular.ui-sortable.js b/docs-web/src/main/webapp/src/lib/angular.ui-sortable.js index 19e7703d..735cc4c9 100644 --- a/docs-web/src/main/webapp/src/lib/angular.ui-sortable.js +++ b/docs-web/src/main/webapp/src/lib/angular.ui-sortable.js @@ -1,111 +1,566 @@ /* - jQuery UI Sortable plugin wrapper + * angular-ui-sortable - This directive allows you to jQueryUI Sortable. + * @version v0.17.2 - 2017-08-17 + * @link http://angular-ui.github.com + * @license MIT + */ +(function(window, angular, undefined) { + 'use strict'; + /* + jQuery UI Sortable plugin wrapper - @param [ui-sortable] {object} Options to pass to $.fn.sortable() merged onto ui.config -*/ -angular.module('ui.sortable', []) - .value('uiSortableConfig',{}) - .directive('uiSortable', [ 'uiSortableConfig', - function(uiSortableConfig) { + @param [ui-sortable] {object} Options to pass to $.fn.sortable() merged onto ui.config + */ + angular.module('ui.sortable', []) + .value('uiSortableConfig',{ + // the default for jquery-ui sortable is "> *", we need to restrict this to + // ng-repeat items + // if the user uses + items: '> [ng-repeat],> [data-ng-repeat],> [x-ng-repeat]' + }) + .directive('uiSortable', [ + 'uiSortableConfig', '$timeout', '$log', + function(uiSortableConfig, $timeout, $log) { return { - require: '?ngModel', + require:'?ngModel', + scope: { + ngModel:'=', + uiSortable:'=', + ////Expression bindings from html. + create:'&uiSortableCreate', + // helper:'&uiSortableHelper', + start:'&uiSortableStart', + activate:'&uiSortableActivate', + // sort:'&uiSortableSort', + // change:'&uiSortableChange', + // over:'&uiSortableOver', + // out:'&uiSortableOut', + beforeStop:'&uiSortableBeforeStop', + update:'&uiSortableUpdate', + remove:'&uiSortableRemove', + receive:'&uiSortableReceive', + deactivate:'&uiSortableDeactivate', + stop:'&uiSortableStop' + }, link: function(scope, element, attrs, ngModel) { + var savedNodes; + var helper; - function combineCallbacks(first,second){ - if( second && (typeof second === "function") ){ - return function(e,ui){ - first(e,ui); - second(e,ui); - }; - } - return first; + function combineCallbacks(first, second){ + var firstIsFunc = typeof first === 'function'; + var secondIsFunc = typeof second === 'function'; + if(firstIsFunc && secondIsFunc) { + return function() { + first.apply(this, arguments); + second.apply(this, arguments); + }; + } else if (secondIsFunc) { + return second; } + return first; + } + + function getSortableWidgetInstance(element) { + // this is a fix to support jquery-ui prior to v1.11.x + // otherwise we should be using `element.sortable('instance')` + var data = element.data('ui-sortable'); + if (data && typeof data === 'object' && data.widgetFullName === 'ui-sortable') { + return data; + } + return null; + } + + function patchSortableOption(key, value) { + if (callbacks[key]) { + if( key === 'stop' ){ + // call apply after stop + value = combineCallbacks( + value, function() { scope.$apply(); }); + + value = combineCallbacks(value, afterStop); + } + // wrap the callback + value = combineCallbacks(callbacks[key], value); + } else if (wrappers[key]) { + value = wrappers[key](value); + } + + // patch the options that need to have values set + if (!value && (key === 'items' || key === 'ui-model-items')) { + value = uiSortableConfig.items; + } + + return value; + } + + function patchUISortableOptions(newVal, oldVal, sortableWidgetInstance) { + function addDummyOptionKey(value, key) { + if (!(key in opts)) { + // add the key in the opts object so that + // the patch function detects and handles it + opts[key] = null; + } + } + // for this directive to work we have to attach some callbacks + angular.forEach(callbacks, addDummyOptionKey); + + // only initialize it in case we have to + // update some options of the sortable + var optsDiff = null; + + if (oldVal) { + // reset deleted options to default + var defaultOptions; + angular.forEach(oldVal, function(oldValue, key) { + if (!newVal || !(key in newVal)) { + if (key in directiveOpts) { + if (key === 'ui-floating') { + opts[key] = 'auto'; + } else { + opts[key] = patchSortableOption(key, undefined); + } + return; + } + + if (!defaultOptions) { + defaultOptions = angular.element.ui.sortable().options; + } + var defaultValue = defaultOptions[key]; + defaultValue = patchSortableOption(key, defaultValue); + + if (!optsDiff) { + optsDiff = {}; + } + optsDiff[key] = defaultValue; + opts[key] = defaultValue; + } + }); + } + + // update changed options + angular.forEach(newVal, function(value, key) { + // if it's a custom option of the directive, + // handle it approprietly + if (key in directiveOpts) { + if (key === 'ui-floating' && (value === false || value === true) && sortableWidgetInstance) { + sortableWidgetInstance.floating = value; + } + + opts[key] = patchSortableOption(key, value); + return; + } + + value = patchSortableOption(key, value); + + if (!optsDiff) { + optsDiff = {}; + } + optsDiff[key] = value; + opts[key] = value; + }); + + return optsDiff; + } + + function getPlaceholderElement (element) { + var placeholder = element.sortable('option','placeholder'); + + // placeholder.element will be a function if the placeholder, has + // been created (placeholder will be an object). If it hasn't + // been created, either placeholder will be false if no + // placeholder class was given or placeholder.element will be + // undefined if a class was given (placeholder will be a string) + if (placeholder && placeholder.element && typeof placeholder.element === 'function') { + var result = placeholder.element(); + // workaround for jquery ui 1.9.x, + // not returning jquery collection + result = angular.element(result); + return result; + } + return null; + } + + function getPlaceholderExcludesludes (element, placeholder) { + // exact match with the placeholder's class attribute to handle + // the case that multiple connected sortables exist and + // the placeholder option equals the class of sortable items + var notCssSelector = opts['ui-model-items'].replace(/[^,]*>/g, ''); + var excludes = element.find('[class="' + placeholder.attr('class') + '"]:not(' + notCssSelector + ')'); + return excludes; + } + + function hasSortingHelper (element, ui) { + var helperOption = element.sortable('option','helper'); + return helperOption === 'clone' || (typeof helperOption === 'function' && ui.item.sortable.isCustomHelperUsed()); + } + + function getSortingHelper (element, ui/*, savedNodes*/) { + var result = null; + if (hasSortingHelper(element, ui) && + element.sortable( 'option', 'appendTo' ) === 'parent') { + // The .ui-sortable-helper element (that's the default class name) + result = helper; + } + return result; + } + + // thanks jquery-ui + function isFloating (item) { + return (/left|right/).test(item.css('float')) || (/inline|table-cell/).test(item.css('display')); + } + + function getElementContext(elementScopes, element) { + for (var i = 0; i < elementScopes.length; i++) { + var c = elementScopes[i]; + if (c.element[0] === element[0]) { + return c; + } + } + } + + function afterStop(e, ui) { + ui.item.sortable._destroy(); + } + + // return the index of ui.item among the items + // we can't just do ui.item.index() because there it might have siblings + // which are not items + function getItemIndex(item) { + return item.parent() + .find(opts['ui-model-items']) + .index(item); + } var opts = {}; - var callbacks = { - receive: null, - remove:null, - start:null, - stop:null, - update:null + // directive specific options + var directiveOpts = { + 'ui-floating': undefined, + 'ui-model-items': uiSortableConfig.items }; - angular.extend(opts, uiSortableConfig); + var callbacks = { + create: null, + start: null, + activate: null, + // sort: null, + // change: null, + // over: null, + // out: null, + beforeStop: null, + update: null, + remove: null, + receive: null, + deactivate: null, + stop: null + }; - if (ngModel) { + var wrappers = { + helper: null + }; - ngModel.$render = function() { - element.sortable( "refresh" ); - }; + angular.extend(opts, directiveOpts, uiSortableConfig, scope.uiSortable); + + if (!angular.element.fn || !angular.element.fn.jquery) { + $log.error('ui.sortable: jQuery should be included before AngularJS!'); + return; + } + + function wireUp () { + // When we add or remove elements, we need the sortable to 'refresh' + // so it can find the new/removed elements. + scope.$watchCollection('ngModel', function() { + // Timeout to let ng-repeat modify the DOM + $timeout(function() { + // ensure that the jquery-ui-sortable widget instance + // is still bound to the directive's element + if (!!getSortableWidgetInstance(element)) { + element.sortable('refresh'); + } + }, 0, false); + }); callbacks.start = function(e, ui) { - // Save position of dragged item - ui.item.sortable = { index: ui.item.index() }; + if (opts['ui-floating'] === 'auto') { + // since the drag has started, the element will be + // absolutely positioned, so we check its siblings + var siblings = ui.item.siblings(); + var sortableWidgetInstance = getSortableWidgetInstance(angular.element(e.target)); + sortableWidgetInstance.floating = isFloating(siblings); + } + + // Save the starting position of dragged item + var index = getItemIndex(ui.item); + ui.item.sortable = { + model: ngModel.$modelValue[index], + index: index, + source: element, + sourceList: ui.item.parent(), + sourceModel: ngModel.$modelValue, + cancel: function () { + ui.item.sortable._isCanceled = true; + }, + isCanceled: function () { + return ui.item.sortable._isCanceled; + }, + isCustomHelperUsed: function () { + return !!ui.item.sortable._isCustomHelperUsed; + }, + _isCanceled: false, + _isCustomHelperUsed: ui.item.sortable._isCustomHelperUsed, + _destroy: function () { + angular.forEach(ui.item.sortable, function(value, key) { + ui.item.sortable[key] = undefined; + }); + }, + _connectedSortables: [], + _getElementContext: function (element) { + return getElementContext(this._connectedSortables, element); + } + }; + }; + + callbacks.activate = function(e, ui) { + var isSourceContext = ui.item.sortable.source === element; + var savedNodesOrigin = isSourceContext ? + ui.item.sortable.sourceList : + element; + var elementContext = { + element: element, + scope: scope, + isSourceContext: isSourceContext, + savedNodesOrigin: savedNodesOrigin + }; + // save the directive's scope so that it is accessible from ui.item.sortable + ui.item.sortable._connectedSortables.push(elementContext); + + // We need to make a copy of the current element's contents so + // we can restore it after sortable has messed it up. + // This is inside activate (instead of start) in order to save + // both lists when dragging between connected lists. + savedNodes = savedNodesOrigin.contents(); + helper = ui.helper; + + // If this list has a placeholder (the connected lists won't), + // don't inlcude it in saved nodes. + var placeholder = getPlaceholderElement(element); + if (placeholder && placeholder.length) { + var excludes = getPlaceholderExcludesludes(element, placeholder); + savedNodes = savedNodes.not(excludes); + } }; callbacks.update = function(e, ui) { - // For some reason the reference to ngModel in stop() is wrong - ui.item.sortable.resort = ngModel; - }; + // Save current drop position but only if this is not a second + // update that happens when moving between lists because then + // the value will be overwritten with the old value + if (!ui.item.sortable.received) { + ui.item.sortable.dropindex = getItemIndex(ui.item); + var droptarget = ui.item.parent().closest('[ui-sortable], [data-ui-sortable], [x-ui-sortable]'); + ui.item.sortable.droptarget = droptarget; + ui.item.sortable.droptargetList = ui.item.parent(); - callbacks.receive = function(e, ui) { - ui.item.sortable.relocate = true; - // added item to array into correct position and set up flag - ngModel.$modelValue.splice(ui.item.index(), 0, ui.item.sortable.moved); - }; + var droptargetContext = ui.item.sortable._getElementContext(droptarget); + ui.item.sortable.droptargetModel = droptargetContext.scope.ngModel; - callbacks.remove = function(e, ui) { - // copy data into item - if (ngModel.$modelValue.length === 1) { - ui.item.sortable.moved = ngModel.$modelValue.splice(0, 1)[0]; - } else { - ui.item.sortable.moved = ngModel.$modelValue.splice(ui.item.sortable.index, 1)[0]; + // Cancel the sort (let ng-repeat do the sort for us) + // Don't cancel if this is the received list because it has + // already been canceled in the other list, and trying to cancel + // here will mess up the DOM. + element.sortable('cancel'); + } + + // Put the nodes back exactly the way they started (this is very + // important because ng-repeat uses comment elements to delineate + // the start and stop of repeat sections and sortable doesn't + // respect their order (even if we cancel, the order of the + // comments are still messed up). + var sortingHelper = !ui.item.sortable.received && getSortingHelper(element, ui, savedNodes); + if (sortingHelper && sortingHelper.length) { + // Restore all the savedNodes except from the sorting helper element. + // That way it will be garbage collected. + savedNodes = savedNodes.not(sortingHelper); + } + var elementContext = ui.item.sortable._getElementContext(element); + savedNodes.appendTo(elementContext.savedNodesOrigin); + + // If this is the target connected list then + // it's safe to clear the restored nodes since: + // update is currently running and + // stop is not called for the target list. + if (ui.item.sortable.received) { + savedNodes = null; + } + + // If received is true (an item was dropped in from another list) + // then we add the new item to this list otherwise wait until the + // stop event where we will know if it was a sort or item was + // moved here from another list + if (ui.item.sortable.received && !ui.item.sortable.isCanceled()) { + scope.$apply(function () { + ngModel.$modelValue.splice(ui.item.sortable.dropindex, 0, + ui.item.sortable.moved); + }); + scope.$emit('ui-sortable:moved', ui); } }; callbacks.stop = function(e, ui) { - // digest all prepared changes - if (ui.item.sortable.resort && !ui.item.sortable.relocate) { + // If the received flag hasn't be set on the item, this is a + // normal sort, if dropindex is set, the item was moved, so move + // the items in the list. + var wasMoved = ('dropindex' in ui.item.sortable) && + !ui.item.sortable.isCanceled(); - // Fetch saved and current position of dropped element - var end, start; - start = ui.item.sortable.index; - end = ui.item.index(); + if (wasMoved && !ui.item.sortable.received) { - // Reorder array and apply change to scope - ui.item.sortable.resort.$modelValue.splice(end, 0, ui.item.sortable.resort.$modelValue.splice(start, 1)[0]); + scope.$apply(function () { + ngModel.$modelValue.splice( + ui.item.sortable.dropindex, 0, + ngModel.$modelValue.splice(ui.item.sortable.index, 1)[0]); + }); + scope.$emit('ui-sortable:moved', ui); + } else if (!wasMoved && + !angular.equals(element.contents().toArray(), savedNodes.toArray())) { + // if the item was not moved + // and the DOM element order has changed, + // then restore the elements + // so that the ngRepeat's comment are correct. + var sortingHelper = getSortingHelper(element, ui, savedNodes); + if (sortingHelper && sortingHelper.length) { + // Restore all the savedNodes except from the sorting helper element. + // That way it will be garbage collected. + savedNodes = savedNodes.not(sortingHelper); + } + var elementContext = ui.item.sortable._getElementContext(element); + savedNodes.appendTo(elementContext.savedNodesOrigin); } - if (ui.item.sortable.resort || ui.item.sortable.relocate) { - scope.$apply(); + + // It's now safe to clear the savedNodes and helper + // since stop is the last callback. + savedNodes = null; + helper = null; + }; + + callbacks.receive = function(e, ui) { + // An item was dropped here from another list, set a flag on the + // item. + ui.item.sortable.received = true; + }; + + callbacks.remove = function(e, ui) { + // Workaround for a problem observed in nested connected lists. + // There should be an 'update' event before 'remove' when moving + // elements. If the event did not fire, cancel sorting. + if (!('dropindex' in ui.item.sortable)) { + element.sortable('cancel'); + ui.item.sortable.cancel(); + } + + // Remove the item from this list's model and copy data into item, + // so the next list can retrive it + if (!ui.item.sortable.isCanceled()) { + scope.$apply(function () { + ui.item.sortable.moved = ngModel.$modelValue.splice( + ui.item.sortable.index, 1)[0]; + }); } }; - } - - - scope.$watch(attrs.uiSortable, function(newVal, oldVal){ - angular.forEach(newVal, function(value, key){ - - if( callbacks[key] ){ - // wrap the callback - value = combineCallbacks( callbacks[key], value ); - } - - element.sortable('option', key, value); + // setup attribute handlers + angular.forEach(callbacks, function(value, key) { + callbacks[key] = combineCallbacks(callbacks[key], + function () { + var attrHandler = scope[key]; + var attrHandlerFn; + if (typeof attrHandler === 'function' && + ('uiSortable' + key.substring(0,1).toUpperCase() + key.substring(1)).length && + typeof (attrHandlerFn = attrHandler()) === 'function') { + attrHandlerFn.apply(this, arguments); + } }); - }, true); - - angular.forEach(callbacks, function(value, key ){ - - opts[key] = combineCallbacks(value, opts[key]); }); - // Create sortable - element.sortable(opts); + wrappers.helper = function (inner) { + if (inner && typeof inner === 'function') { + return function (e, item) { + var oldItemSortable = item.sortable; + var index = getItemIndex(item); + item.sortable = { + model: ngModel.$modelValue[index], + index: index, + source: element, + sourceList: item.parent(), + sourceModel: ngModel.$modelValue, + _restore: function () { + angular.forEach(item.sortable, function(value, key) { + item.sortable[key] = undefined; + }); + + item.sortable = oldItemSortable; + } + }; + + var innerResult = inner.apply(this, arguments); + item.sortable._restore(); + item.sortable._isCustomHelperUsed = item !== innerResult; + return innerResult; + }; + } + return inner; + }; + + scope.$watchCollection('uiSortable', function(newVal, oldVal) { + // ensure that the jquery-ui-sortable widget instance + // is still bound to the directive's element + var sortableWidgetInstance = getSortableWidgetInstance(element); + if (!!sortableWidgetInstance) { + var optsDiff = patchUISortableOptions(newVal, oldVal, sortableWidgetInstance); + + if (optsDiff) { + element.sortable('option', optsDiff); + } + } + }, true); + + patchUISortableOptions(opts); + } + + function init () { + if (ngModel) { + wireUp(); + } else { + $log.info('ui.sortable: ngModel not provided!', element); + } + + // Create sortable + element.sortable(opts); + } + + function initIfEnabled () { + if (scope.uiSortable && scope.uiSortable.disabled) { + return false; + } + + init(); + + // Stop Watcher + initIfEnabled.cancelWatcher(); + initIfEnabled.cancelWatcher = angular.noop; + + return true; + } + + initIfEnabled.cancelWatcher = angular.noop; + + if (!initIfEnabled()) { + initIfEnabled.cancelWatcher = scope.$watch('uiSortable.disabled', initIfEnabled); + } } }; } -]); \ No newline at end of file + ]); + +})(window, window.angular); \ No newline at end of file diff --git a/docs-web/src/main/webapp/src/lib/angular.ui-utils.js b/docs-web/src/main/webapp/src/lib/angular.ui-utils.js deleted file mode 100644 index dab960fd..00000000 --- a/docs-web/src/main/webapp/src/lib/angular.ui-utils.js +++ /dev/null @@ -1,7 +0,0 @@ -/** - * angular-ui-utils - Swiss-Army-Knife of AngularJS tools (with no external dependencies!) - * @version v0.1.0 - 2013-12-30 - * @link http://angular-ui.github.com - * @license MIT License, http://www.opensource.org/licenses/MIT - */ -"use strict";angular.module("ui.alias",[]).config(["$compileProvider","uiAliasConfig",function(a,b){b=b||{},angular.forEach(b,function(b,c){angular.isString(b)&&(b={replace:!0,template:b}),a.directive(c,function(){return b})})}]),angular.module("ui.event",[]).directive("uiEvent",["$parse",function(a){return function(b,c,d){var e=b.$eval(d.uiEvent);angular.forEach(e,function(d,e){var f=a(d);c.bind(e,function(a){var c=Array.prototype.slice.call(arguments);c=c.splice(1),f(b,{$event:a,$params:c}),b.$$phase||b.$apply()})})}}]),angular.module("ui.format",[]).filter("format",function(){return function(a,b){var c=a;if(angular.isString(c)&&void 0!==b)if(angular.isArray(b)||angular.isObject(b)||(b=[b]),angular.isArray(b)){var d=b.length,e=function(a,c){return c=parseInt(c,10),c>=0&&d>c?b[c]:a};c=c.replace(/\$([0-9]+)/g,e)}else angular.forEach(b,function(a,b){c=c.split(":"+b).join(a)});return c}}),angular.module("ui.highlight",[]).filter("highlight",function(){return function(a,b,c){return b||angular.isNumber(b)?(a=a.toString(),b=b.toString(),c?a.split(b).join(''+b+""):a.replace(new RegExp(b,"gi"),'$&')):a}}),angular.module("ui.include",[]).directive("uiInclude",["$http","$templateCache","$anchorScroll","$compile",function(a,b,c,d){return{restrict:"ECA",terminal:!0,compile:function(e,f){var g=f.uiInclude||f.src,h=f.fragment||"",i=f.onload||"",j=f.autoscroll;return function(e,f){function k(){var k=++m,o=e.$eval(g),p=e.$eval(h);o?a.get(o,{cache:b}).success(function(a){if(k===m){l&&l.$destroy(),l=e.$new();var b;b=p?angular.element("
          ").html(a).find(p):angular.element("
          ").html(a).contents(),f.html(b),d(b)(l),!angular.isDefined(j)||j&&!e.$eval(j)||c(),l.$emit("$includeContentLoaded"),e.$eval(i)}}).error(function(){k===m&&n()}):n()}var l,m=0,n=function(){l&&(l.$destroy(),l=null),f.html("")};e.$watch(h,k),e.$watch(g,k)}}}}]),angular.module("ui.indeterminate",[]).directive("uiIndeterminate",[function(){return{compile:function(a,b){return b.type&&"checkbox"===b.type.toLowerCase()?function(a,b,c){a.$watch(c.uiIndeterminate,function(a){b[0].indeterminate=!!a})}:angular.noop}}}]),angular.module("ui.inflector",[]).filter("inflector",function(){function a(a){return a.replace(/^([a-z])|\s+([a-z])/g,function(a){return a.toUpperCase()})}function b(a,b){return a.replace(/[A-Z]/g,function(a){return b+a})}var c={humanize:function(c){return a(b(c," ").split("_").join(" "))},underscore:function(a){return a.substr(0,1).toLowerCase()+b(a.substr(1),"_").toLowerCase().split(" ").join("_")},variable:function(b){return b=b.substr(0,1).toLowerCase()+a(b.split("_").join(" ")).substr(1).split(" ").join("")}};return function(a,b){return b!==!1&&angular.isString(a)?(b=b||"humanize",c[b](a)):a}}),angular.module("ui.jq",[]).value("uiJqConfig",{}).directive("uiJq",["uiJqConfig","$timeout",function(a,b){return{restrict:"A",compile:function(c,d){if(!angular.isFunction(c[d.uiJq]))throw new Error('ui-jq: The "'+d.uiJq+'" function does not exist');var e=a&&a[d.uiJq];return function(a,c,d){function f(){b(function(){c[d.uiJq].apply(c,g)},0,!1)}var g=[];d.uiOptions?(g=a.$eval("["+d.uiOptions+"]"),angular.isObject(e)&&angular.isObject(g[0])&&(g[0]=angular.extend({},e,g[0]))):e&&(g=[e]),d.ngModel&&c.is("select,input,textarea")&&c.bind("change",function(){c.trigger("input")}),d.uiRefresh&&a.$watch(d.uiRefresh,function(){f()}),f()}}}}]),angular.module("ui.keypress",[]).factory("keypressHelper",["$parse",function(a){var b={8:"backspace",9:"tab",13:"enter",27:"esc",32:"space",33:"pageup",34:"pagedown",35:"end",36:"home",37:"left",38:"up",39:"right",40:"down",45:"insert",46:"delete"},c=function(a){return a.charAt(0).toUpperCase()+a.slice(1)};return function(d,e,f,g){var h,i=[];h=e.$eval(g["ui"+c(d)]),angular.forEach(h,function(b,c){var d,e;e=a(b),angular.forEach(c.split(" "),function(a){d={expression:e,keys:{}},angular.forEach(a.split("-"),function(a){d.keys[a]=!0}),i.push(d)})}),f.bind(d,function(a){var c=!(!a.metaKey||a.ctrlKey),f=!!a.altKey,g=!!a.ctrlKey,h=!!a.shiftKey,j=a.keyCode;"keypress"===d&&!h&&j>=97&&122>=j&&(j-=32),angular.forEach(i,function(d){var i=d.keys[b[j]]||d.keys[j.toString()],k=!!d.keys.meta,l=!!d.keys.alt,m=!!d.keys.ctrl,n=!!d.keys.shift;i&&k===c&&l===f&&m===g&&n===h&&e.$apply(function(){d.expression(e,{$event:a})})})})}}]),angular.module("ui.keypress").directive("uiKeydown",["keypressHelper",function(a){return{link:function(b,c,d){a("keydown",b,c,d)}}}]),angular.module("ui.keypress").directive("uiKeypress",["keypressHelper",function(a){return{link:function(b,c,d){a("keypress",b,c,d)}}}]),angular.module("ui.keypress").directive("uiKeyup",["keypressHelper",function(a){return{link:function(b,c,d){a("keyup",b,c,d)}}}]),angular.module("ui.mask",[]).value("uiMaskConfig",{maskDefinitions:{9:/\d/,A:/[a-zA-Z]/,"*":/[a-zA-Z0-9]/}}).directive("uiMask",["uiMaskConfig",function(a){return{priority:100,require:"ngModel",restrict:"A",compile:function(){var b=a;return function(a,c,d,e){function f(a){return angular.isDefined(a)?(s(a),N?(k(),l(),!0):j()):j()}function g(a){angular.isDefined(a)&&(D=a,N&&w())}function h(a){return N?(G=o(a||""),I=n(G),e.$setValidity("mask",I),I&&G.length?p(G):void 0):a}function i(a){return N?(G=o(a||""),I=n(G),e.$viewValue=G.length?p(G):"",e.$setValidity("mask",I),""===G&&void 0!==e.$error.required&&e.$setValidity("required",!1),I?G:void 0):a}function j(){return N=!1,m(),angular.isDefined(P)?c.attr("placeholder",P):c.removeAttr("placeholder"),angular.isDefined(Q)?c.attr("maxlength",Q):c.removeAttr("maxlength"),c.val(e.$modelValue),e.$viewValue=e.$modelValue,!1}function k(){G=K=o(e.$modelValue||""),H=J=p(G),I=n(G);var a=I&&G.length?H:"";d.maxlength&&c.attr("maxlength",2*B[B.length-1]),c.attr("placeholder",D),c.val(a),e.$viewValue=a}function l(){O||(c.bind("blur",t),c.bind("mousedown mouseup",u),c.bind("input keyup click focus",w),O=!0)}function m(){O&&(c.unbind("blur",t),c.unbind("mousedown",u),c.unbind("mouseup",u),c.unbind("input",w),c.unbind("keyup",w),c.unbind("click",w),c.unbind("focus",w),O=!1)}function n(a){return a.length?a.length>=F:!0}function o(a){var b="",c=C.slice();return a=a.toString(),angular.forEach(E,function(b){a=a.replace(b,"")}),angular.forEach(a.split(""),function(a){c.length&&c[0].test(a)&&(b+=a,c.shift())}),b}function p(a){var b="",c=B.slice();return angular.forEach(D.split(""),function(d,e){a.length&&e===c[0]?(b+=a.charAt(0)||"_",a=a.substr(1),c.shift()):b+=d}),b}function q(a){var b=d.placeholder;return"undefined"!=typeof b&&b[a]?b[a]:"_"}function r(){return D.replace(/[_]+/g,"_").replace(/([^_]+)([a-zA-Z0-9])([^_])/g,"$1$2_$3").split("_")}function s(a){var b=0;if(B=[],C=[],D="","string"==typeof a){F=0;var c=!1,d=a.split("");angular.forEach(d,function(a,d){R.maskDefinitions[a]?(B.push(b),D+=q(d),C.push(R.maskDefinitions[a]),b++,c||F++):"?"===a?c=!0:(D+=a,b++)})}B.push(B.slice().pop()+1),E=r(),N=B.length>1?!0:!1}function t(){L=0,M=0,I&&0!==G.length||(H="",c.val(""),a.$apply(function(){e.$setViewValue("")}))}function u(a){"mousedown"===a.type?c.bind("mouseout",v):c.unbind("mouseout",v)}function v(){M=A(this),c.unbind("mouseout",v)}function w(b){b=b||{};var d=b.which,f=b.type;if(16!==d&&91!==d){var g,h=c.val(),i=J,j=o(h),k=K,l=!1,m=y(this)||0,n=L||0,q=m-n,r=B[0],s=B[j.length]||B.slice().shift(),t=M||0,u=A(this)>0,v=t>0,w=h.length>i.length||t&&h.length>i.length-t,C=h.length=37&&40>=d&&b.shiftKey,E=37===d,F=8===d||"keyup"!==f&&C&&-1===q,G=46===d||"keyup"!==f&&C&&0===q&&!v,H=(E||F||"click"===f)&&m>r;if(M=A(this),!D&&(!u||"click"!==f&&"keyup"!==f)){if("input"===f&&C&&!v&&j===k){for(;F&&m>r&&!x(m);)m--;for(;G&&s>m&&-1===B.indexOf(m);)m++;var I=B.indexOf(m);j=j.substring(0,I)+j.substring(I+1),l=!0}for(g=p(j),J=g,K=j,c.val(g),l&&a.$apply(function(){e.$setViewValue(j)}),w&&r>=m&&(m=r+1),H&&m--,m=m>s?s:r>m?r:m;!x(m)&&m>r&&s>m;)m+=H?-1:1;(H&&s>m||w&&!x(n))&&m++,L=m,z(this,m)}}}function x(a){return B.indexOf(a)>-1}function y(a){if(!a)return 0;if(void 0!==a.selectionStart)return a.selectionStart;if(document.selection){a.focus();var b=document.selection.createRange();return b.moveStart("character",-a.value.length),b.text.length}return 0}function z(a,b){if(!a)return 0;if(0!==a.offsetWidth&&0!==a.offsetHeight)if(a.setSelectionRange)a.focus(),a.setSelectionRange(b,b);else if(a.createTextRange){var c=a.createTextRange();c.collapse(!0),c.moveEnd("character",b),c.moveStart("character",b),c.select()}}function A(a){return a?void 0!==a.selectionStart?a.selectionEnd-a.selectionStart:document.selection?document.selection.createRange().text.length:0:0}var B,C,D,E,F,G,H,I,J,K,L,M,N=!1,O=!1,P=d.placeholder,Q=d.maxlength,R={};d.uiOptions?(R=a.$eval("["+d.uiOptions+"]"),angular.isObject(R[0])&&(R=function(a,b){for(var c in a)Object.prototype.hasOwnProperty.call(a,c)&&(b[c]?angular.extend(b[c],a[c]):b[c]=angular.copy(a[c]));return b}(b,R[0]))):R=b,d.$observe("uiMask",f),d.$observe("placeholder",g),e.$formatters.push(h),e.$parsers.push(i),c.bind("mousedown mouseup",u),Array.prototype.indexOf||(Array.prototype.indexOf=function(a){if(null===this)throw new TypeError;var b=Object(this),c=b.length>>>0;if(0===c)return-1;var d=0;if(arguments.length>1&&(d=Number(arguments[1]),d!==d?d=0:0!==d&&1/0!==d&&d!==-1/0&&(d=(d>0||-1)*Math.floor(Math.abs(d)))),d>=c)return-1;for(var e=d>=0?d:Math.max(c-Math.abs(d),0);c>e;e++)if(e in b&&b[e]===a)return e;return-1})}}}}]),angular.module("ui.reset",[]).value("uiResetConfig",null).directive("uiReset",["uiResetConfig",function(a){var b=null;return void 0!==a&&(b=a),{require:"ngModel",link:function(a,c,d,e){var f;f=angular.element(''),c.wrap('').after(f),f.bind("click",function(c){c.preventDefault(),a.$apply(function(){d.uiReset?e.$setViewValue(a.$eval(d.uiReset)):e.$setViewValue(b),e.$render()})})}}}]),angular.module("ui.route",[]).directive("uiRoute",["$location","$parse",function(a,b){return{restrict:"AC",scope:!0,compile:function(c,d){var e;if(d.uiRoute)e="uiRoute";else if(d.ngHref)e="ngHref";else{if(!d.href)throw new Error("uiRoute missing a route or href property on "+c[0]);e="href"}return function(c,d,f){function g(b){var d=b.indexOf("#");d>-1&&(b=b.substr(d+1)),(j=function(){i(c,a.path().indexOf(b)>-1)})()}function h(b){var d=b.indexOf("#");d>-1&&(b=b.substr(d+1)),(j=function(){var d=new RegExp("^"+b+"$",["i"]);i(c,d.test(a.path()))})()}var i=b(f.ngModel||f.routeModel||"$uiRoute").assign,j=angular.noop;switch(e){case"uiRoute":f.uiRoute?h(f.uiRoute):f.$observe("uiRoute",h);break;case"ngHref":f.ngHref?g(f.ngHref):f.$observe("ngHref",g);break;case"href":g(f.href)}c.$on("$routeChangeSuccess",function(){j()}),c.$on("$stateChangeSuccess",function(){j()})}}}}]),angular.module("ui.scroll.jqlite",["ui.scroll"]).service("jqLiteExtras",["$log","$window",function(a,b){return{registerFor:function(a){var c,d,e,f,g,h,i;return d=angular.element.prototype.css,a.prototype.css=function(a,b){var c,e;return e=this,c=e[0],c&&3!==c.nodeType&&8!==c.nodeType&&c.style?d.call(e,a,b):void 0},h=function(a){return a&&a.document&&a.location&&a.alert&&a.setInterval},i=function(a,b,c){var d,e,f,g,i;return d=a[0],i={top:["scrollTop","pageYOffset","scrollLeft"],left:["scrollLeft","pageXOffset","scrollTop"]}[b],e=i[0],g=i[1],f=i[2],h(d)?angular.isDefined(c)?d.scrollTo(a[f].call(a),c):g in d?d[g]:d.document.documentElement[e]:angular.isDefined(c)?d[e]=c:d[e]},b.getComputedStyle?(f=function(a){return b.getComputedStyle(a,null)},c=function(a,b){return parseFloat(b)}):(f=function(a){return a.currentStyle},c=function(a,b){var c,d,e,f,g,h,i;return c=/[+-]?(?:\d*\.|)\d+(?:[eE][+-]?\d+|)/.source,f=new RegExp("^("+c+")(?!px)[a-z%]+$","i"),f.test(b)?(i=a.style,d=i.left,g=a.runtimeStyle,h=g&&g.left,g&&(g.left=i.left),i.left=b,e=i.pixelLeft,i.left=d,h&&(g.left=h),e):parseFloat(b)}),e=function(a,b){var d,e,g,i,j,k,l,m,n,o,p,q,r;return h(a)?(d=document.documentElement[{height:"clientHeight",width:"clientWidth"}[b]],{base:d,padding:0,border:0,margin:0}):(r={width:[a.offsetWidth,"Left","Right"],height:[a.offsetHeight,"Top","Bottom"]}[b],d=r[0],l=r[1],m=r[2],k=f(a),p=c(a,k["padding"+l])||0,q=c(a,k["padding"+m])||0,e=c(a,k["border"+l+"Width"])||0,g=c(a,k["border"+m+"Width"])||0,i=k["margin"+l],j=k["margin"+m],n=c(a,i)||0,o=c(a,j)||0,{base:d,padding:p+q,border:e+g,margin:n+o})},g=function(a,b,c){var d,g,h;return g=e(a,b),g.base>0?{base:g.base-g.padding-g.border,outer:g.base,outerfull:g.base+g.margin}[c]:(d=f(a),h=d[b],(0>h||null===h)&&(h=a.style[b]||0),h=parseFloat(h)||0,{base:h-g.padding-g.border,outer:h,outerfull:h+g.padding+g.border+g.margin}[c])},angular.forEach({before:function(a){var b,c,d,e,f,g,h;if(f=this,c=f[0],e=f.parent(),b=e.contents(),b[0]===c)return e.prepend(a);for(d=g=1,h=b.length-1;h>=1?h>=g:g>=h;d=h>=1?++g:--g)if(b[d]===c)return angular.element(b[d-1]).after(a),void 0;throw new Error("invalid DOM structure "+c.outerHTML)},height:function(a){var b;return b=this,angular.isDefined(a)?(angular.isNumber(a)&&(a+="px"),d.call(b,"height",a)):g(this[0],"height","base")},outerHeight:function(a){return g(this[0],"height",a?"outerfull":"outer")},offset:function(a){var b,c,d,e,f,g;return f=this,arguments.length?void 0===a?f:a:(b={top:0,left:0},e=f[0],(c=e&&e.ownerDocument)?(d=c.documentElement,e.getBoundingClientRect&&(b=e.getBoundingClientRect()),g=c.defaultView||c.parentWindow,{top:b.top+(g.pageYOffset||d.scrollTop)-(d.clientTop||0),left:b.left+(g.pageXOffset||d.scrollLeft)-(d.clientLeft||0)}):void 0)},scrollTop:function(a){return i(this,"top",a)},scrollLeft:function(a){return i(this,"left",a)}},function(b,c){return a.prototype[c]?void 0:a.prototype[c]=b})}}}]).run(["$log","$window","jqLiteExtras",function(a,b,c){return b.jQuery?void 0:c.registerFor(angular.element)}]),angular.module("ui.scroll",[]).directive("ngScrollViewport",["$log",function(){return{controller:["$scope","$element",function(a,b){return b}]}}]).directive("ngScroll",["$log","$injector","$rootScope","$timeout",function(a,b,c,d){return{require:["?^ngScrollViewport"],transclude:"element",priority:1e3,terminal:!0,compile:function(e,f,g){return function(f,h,i,j){var k,l,m,n,o,p,q,r,s,t,u,v,w,x,y,z,A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R,S,T;if(H=i.ngScroll.match(/^\s*(\w+)\s+in\s+(\w+)\s*$/),!H)throw new Error('Expected ngScroll in form of "item_ in _datasource_" but got "'+i.ngScroll+'"');if(F=H[1],v=H[2],D=function(a){return angular.isObject(a)&&a.get&&angular.isFunction(a.get)},u=f[v],!D(u)&&(u=b.get(v),!D(u)))throw new Error(v+" is not a valid datasource");return r=Math.max(3,+i.bufferSize||10),q=function(){return T.height()*Math.max(.1,+i.padding||.1)},O=function(a){return a[0].scrollHeight||a[0].document.documentElement.scrollHeight},k=null,g(R=f.$new(),function(a){var b,c,d,f,g,h;if(f=a[0].localName,"dl"===f)throw new Error("ng-scroll directive does not support <"+a[0].localName+"> as a repeating tag: "+a[0].outerHTML);return"li"!==f&&"tr"!==f&&(f="div"),h=j[0]||angular.element(window),h.css({"overflow-y":"auto",display:"block"}),d=function(a){var b,c,d;switch(a){case"tr":return d=angular.element("
          "),b=d.find("div"),c=d.find("tr"),c.paddingHeight=function(){return b.height.apply(b,arguments)},c;default:return c=angular.element("<"+a+">"),c.paddingHeight=c.height,c}},c=function(a,b,c){return b[{top:"before",bottom:"after"}[c]](a),{paddingHeight:function(){return a.paddingHeight.apply(a,arguments)},insert:function(b){return a[{top:"after",bottom:"before"}[c]](b)}}},g=c(d(f),e,"top"),b=c(d(f),e,"bottom"),R.$destroy(),k={viewport:h,topPadding:g.paddingHeight,bottomPadding:b.paddingHeight,append:b.insert,prepend:g.insert,bottomDataPos:function(){return O(h)-b.paddingHeight()},topDataPos:function(){return g.paddingHeight()}}}),T=k.viewport,B=1,I=1,p=[],J=[],x=!1,n=!1,G=u.loading||function(){},E=!1,L=function(a,b){var c,d;for(c=d=a;b>=a?b>d:d>b;c=b>=a?++d:--d)p[c].scope.$destroy(),p[c].element.remove();return p.splice(a,b-a)},K=function(){return B=1,I=1,L(0,p.length),k.topPadding(0),k.bottomPadding(0),J=[],x=!1,n=!1,l(!1)},o=function(){return T.scrollTop()+T.height()},S=function(){return T.scrollTop()},P=function(){return!x&&k.bottomDataPos()=g?0>=f:f>=0)&&(d=p[c].element.outerHeight(!0),k.bottomDataPos()-b-d>o()+q());c=0>=g?++f:--f)b+=d,e++,x=!1;return e>0?(k.bottomPadding(k.bottomPadding()+b),L(p.length-e,p.length),I-=e,a.log("clipped off bottom "+e+" bottom padding "+k.bottomPadding())):void 0},Q=function(){return!n&&k.topDataPos()>S()-q()},t=function(){var b,c,d,e,f,g;for(e=0,d=0,f=0,g=p.length;g>f&&(b=p[f],c=b.element.outerHeight(!0),k.topDataPos()+e+c0?(k.topPadding(k.topPadding()+e),L(0,d),B+=d,a.log("clipped off top "+d+" top padding "+k.topPadding())):void 0},w=function(a,b){return E||(E=!0,G(!0)),1===J.push(a)?z(b):void 0},C=function(a,b){var c,d,e;return c=f.$new(),c[F]=b,d=a>B,c.$index=a,d&&c.$index--,e={scope:c},g(c,function(b){return e.element=b,d?a===I?(k.append(b),p.push(e)):(p[a-B].element.after(b),p.splice(a-B+1,0,e)):(k.prepend(b),p.unshift(e))}),{appended:d,wrapper:e}},m=function(a,b){var c;return a?k.bottomPadding(Math.max(0,k.bottomPadding()-b.element.outerHeight(!0))):(c=k.topPadding()-b.element.outerHeight(!0),c>=0?k.topPadding(c):T.scrollTop(T.scrollTop()+b.element.outerHeight(!0)))},l=function(b,c,e){var f;return f=function(){return a.log("top {actual="+k.topDataPos()+" visible from="+S()+" bottom {visible through="+o()+" actual="+k.bottomDataPos()+"}"),P()?w(!0,b):Q()&&w(!1,b),e?e():void 0},c?d(function(){var a,b,d;for(b=0,d=c.length;d>b;b++)a=c[b],m(a.appended,a.wrapper);return f()}):f()},A=function(a,b){return l(a,b,function(){return J.shift(),0===J.length?(E=!1,G(!1)):z(a)})},z=function(b){var c;return c=J[0],c?p.length&&!P()?A(b):u.get(I,r,function(c){var d,e,f,g;if(e=[],0===c.length)x=!0,k.bottomPadding(0),a.log("appended: requested "+r+" records starting from "+I+" recieved: eof");else{for(t(),f=0,g=c.length;g>f;f++)d=c[f],e.push(C(++I,d));a.log("appended: requested "+r+" received "+c.length+" buffer size "+p.length+" first "+B+" next "+I)}return A(b,e)}):p.length&&!Q()?A(b):u.get(B-r,r,function(c){var d,e,f,g;if(e=[],0===c.length)n=!0,k.topPadding(0),a.log("prepended: requested "+r+" records starting from "+(B-r)+" recieved: bof");else{for(s(),d=f=g=c.length-1;0>=g?0>=f:f>=0;d=0>=g?++f:--f)e.unshift(C(--B,c[d]));a.log("prepended: requested "+r+" received "+c.length+" buffer size "+p.length+" first "+B+" next "+I)}return A(b,e)})},M=function(){return c.$$phase||E?void 0:(l(!1),f.$apply())},T.bind("resize",M),N=function(){return c.$$phase||E?void 0:(l(!0),f.$apply())},T.bind("scroll",N),f.$watch(u.revision,function(){return K()}),y=u.scope?u.scope.$new():f.$new(),f.$on("$destroy",function(){return y.$destroy(),T.unbind("resize",M),T.unbind("scroll",N)}),y.$on("update.items",function(a,b,c){var d,e,f,g,h;if(angular.isFunction(b))for(e=function(a){return b(a.scope)},f=0,g=p.length;g>f;f++)d=p[f],e(d);else 0<=(h=b-B-1)&&hh;h++)d=p[h],e.unshift(d);for(g=function(a){return b(a.scope)?(L(e.length-1-c,e.length-c),I--):void 0},c=i=0,m=e.length;m>i;c=++i)f=e[c],g(f)}else 0<=(o=b-B-1)&&oj;c=++j)d=p[c],d.scope.$index=B+c;return l(!1)}),y.$on("insert.item",function(a,b,c){var d,e,f,g,h,i,j,k,m,n,o,q;if(e=[],angular.isFunction(b)){for(f=[],i=0,m=p.length;m>i;i++)c=p[i],f.unshift(c);for(h=function(a){var f,g,h,i,j;if(g=b(a.scope)){if(C=function(a,b){return C(a,b),I++},angular.isArray(g)){for(j=[],f=h=0,i=g.length;i>h;f=++h)c=g[f],j.push(e.push(C(d+f,c)));return j}return e.push(C(d,g))}},d=j=0,n=f.length;n>j;d=++j)g=f[d],h(g)}else 0<=(q=b-B-1)&&qk;d=++k)c=p[d],c.scope.$index=B+d;return l(!1,e)})}}}}]),angular.module("ui.scrollfix",[]).directive("uiScrollfix",["$window",function(a){return{require:"^?uiScrollfixTarget",link:function(b,c,d,e){function f(){var b;if(angular.isDefined(a.pageYOffset))b=a.pageYOffset;else{var e=document.compatMode&&"BackCompat"!==document.compatMode?document.documentElement:document.body;b=e.scrollTop}!c.hasClass("ui-scrollfix")&&b>d.uiScrollfix?c.addClass("ui-scrollfix"):c.hasClass("ui-scrollfix")&&b + * @example + * @example + * @example + * @example + * @example + * + * @param ui-validate {string|object literal} If strings is passed it should be a scope's function to be used as a validator. + * If an object literal is passed a key denotes a validation error key while a value should be a validator function. + * In both cases validator function should take a value to validate as its argument and should return true/false indicating a validation result. + * It is possible for a validator function to return a promise, however promises are better handled by ui-validate-async. + * + * @param ui-validate-async {string|object literal} If strings is passed it should be a scope's function to be used as a validator. + * If an object literal is passed a key denotes a validation error key while a value should be a validator function. + * Async validator function should take a value to validate as its argument and should return a promise that resolves if valid and reject if not, + * indicating a validation result. + * ui-validate-async supports non asyncronous validators. They are wrapped into a promise. Although is recomented to use ui-validate instead, since + * all validations declared in ui-validate-async are registered un ngModel.$asyncValidators that runs after ngModel.$validators if and only if + * all validators in ngModel.$validators reports as valid. + */ + angular.module('ui.validate',[]) + .directive('uiValidate', ['$$uiValidateApplyWatch', '$$uiValidateApplyWatchCollection', function ($$uiValidateApplyWatch, $$uiValidateApplyWatchCollection) { + + return { + restrict: 'A', + require: 'ngModel', + link: function(scope, elm, attrs, ctrl) { + var validateFn, validateExpr = scope.$eval(attrs.uiValidate); + + if (!validateExpr) { + return; + } + + if (angular.isString(validateExpr)) { + validateExpr = { + validator: validateExpr + }; + } + + angular.forEach(validateExpr, function(exprssn, key) { + validateFn = function(modelValue, viewValue) { + // $value is left for retrocompatibility + var expression = scope.$eval(exprssn, { + '$value': modelValue, + '$modelValue': modelValue, + '$viewValue': viewValue, + '$name': ctrl.$name + }); + // Keep support for promises for retrocompatibility + if (angular.isObject(expression) && angular.isFunction(expression.then)) { + expression.then(function() { + ctrl.$setValidity(key, true); + }, function() { + ctrl.$setValidity(key, false); + }); + // Return as valid for now. Validity is updated when promise resolves. + return true; + } else { + return !!expression; // Transform 'undefined' to false (to avoid corrupting the NgModelController and the FormController) + } + }; + ctrl.$validators[key] = validateFn; + }); + + // Support for ui-validate-watch + if (attrs.uiValidateWatch) { + $$uiValidateApplyWatch(scope, ctrl, scope.$eval(attrs.uiValidateWatch), attrs.uiValidateWatchObjectEquality); + } + if (attrs.uiValidateWatchCollection) { + $$uiValidateApplyWatchCollection(scope, ctrl, scope.$eval(attrs.uiValidateWatchCollection)); + } + } + }; + }]) + .directive('uiValidateAsync', ['$$uiValidateApplyWatch', '$$uiValidateApplyWatchCollection', '$timeout', '$q', function ($$uiValidateApplyWatch, $$uiValidateApplyWatchCollection, $timeout, $q) { + + return { + restrict: 'A', + require: 'ngModel', + link: function (scope, elm, attrs, ctrl) { + var validateFn, validateExpr = scope.$eval(attrs.uiValidateAsync); + + if (!validateExpr){ return;} + + if (angular.isString(validateExpr)) { + validateExpr = { validatorAsync: validateExpr }; + } + + angular.forEach(validateExpr, function (exprssn, key) { + validateFn = function(modelValue, viewValue) { + // $value is left for ease of use + var expression = scope.$eval(exprssn, { + '$value': modelValue, + '$modelValue': modelValue, + '$viewValue': viewValue, + '$name': ctrl.$name + }); + // Check if it's a promise + if (angular.isObject(expression) && angular.isFunction(expression.then)) { + return expression; + // Support for validate non-async validators + } else { + return $q(function(resolve, reject) { + setTimeout(function() { + if (expression) { + resolve(); + } else { + reject(); + } + }, 0); + }); + } + }; + ctrl.$asyncValidators[key] = validateFn; + }); + + // Support for ui-validate-watch + if (attrs.uiValidateWatch){ + $$uiValidateApplyWatch( scope, ctrl, scope.$eval(attrs.uiValidateWatch), attrs.uiValidateWatchObjectEquality); + } + if (attrs.uiValidateWatchCollection) { + $$uiValidateApplyWatchCollection(scope, ctrl, scope.$eval(attrs.uiValidateWatchCollection)); + } + } + }; + }]) + .service('$$uiValidateApplyWatch', function () { + return function (scope, ctrl, watch, objectEquality) { + var watchCallback = function () { + ctrl.$validate(); + }; + + //string - update all validators on expression change + if (angular.isString(watch)) { + scope.$watch(watch, watchCallback, objectEquality); + //array - update all validators on change of any expression + } else if (angular.isArray(watch)) { + angular.forEach(watch, function (expression) { + scope.$watch(expression, watchCallback, objectEquality); + }); + //object - update appropriate validator + } else if (angular.isObject(watch)) { + angular.forEach(watch, function (expression/*, validatorKey*/) { + //value is string - look after one expression + if (angular.isString(expression)) { + scope.$watch(expression, watchCallback, objectEquality); + } + //value is array - look after all expressions in array + if (angular.isArray(expression)) { + angular.forEach(expression, function (intExpression) { + scope.$watch(intExpression, watchCallback, objectEquality); + }); + } + }); + } + }; + }) + .service('$$uiValidateApplyWatchCollection', function () { + return function (scope, ctrl, watch) { + var watchCallback = function () { + ctrl.$validate(); + }; + + //string - update all validators on expression change + if (angular.isString(watch)) { + scope.$watchCollection(watch, watchCallback); + //array - update all validators on change of any expression + } else if (angular.isArray(watch)) { + angular.forEach(watch, function (expression) { + scope.$watchCollection(expression, watchCallback); + }); + //object - update appropriate validator + } else if (angular.isObject(watch)) { + angular.forEach(watch, function (expression/*, validatorKey*/) { + //value is string - look after one expression + if (angular.isString(expression)) { + scope.$watchCollection(expression, watchCallback); + } + //value is array - look after all expressions in array + if (angular.isArray(expression)) { + angular.forEach(expression, function (intExpression) { + scope.$watchCollection(intExpression, watchCallback); + }); + } + }); + } + }; + }); + +}()); \ No newline at end of file diff --git a/docs-web/src/main/webapp/src/lib/jquery.js b/docs-web/src/main/webapp/src/lib/jquery.js index 2c32d17f..854cc4e0 100644 --- a/docs-web/src/main/webapp/src/lib/jquery.js +++ b/docs-web/src/main/webapp/src/lib/jquery.js @@ -1,5 +1,10253 @@ -/*! jQuery v2.0.3 | (c) 2005, 2013 jQuery Foundation, Inc. | jquery.org/license -*/ -(function(e,undefined){var t,n,r=typeof undefined,i=e.location,o=e.document,s=o.documentElement,a=e.jQuery,u=e.$,l={},c=[],p="2.0.3",f=c.concat,h=c.push,d=c.slice,g=c.indexOf,m=l.toString,y=l.hasOwnProperty,v=p.trim,x=function(e,n){return new x.fn.init(e,n,t)},b=/[+-]?(?:\d*\.|)\d+(?:[eE][+-]?\d+|)/.source,w=/\S+/g,T=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]*))$/,C=/^<(\w+)\s*\/?>(?:<\/\1>|)$/,k=/^-ms-/,N=/-([\da-z])/gi,E=function(e,t){return t.toUpperCase()},S=function(){o.removeEventListener("DOMContentLoaded",S,!1),e.removeEventListener("load",S,!1),x.ready()};x.fn=x.prototype={jquery:p,constructor:x,init:function(e,t,n){var r,i;if(!e)return this;if("string"==typeof e){if(r="<"===e.charAt(0)&&">"===e.charAt(e.length-1)&&e.length>=3?[null,e,null]:T.exec(e),!r||!r[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(r[1]){if(t=t instanceof x?t[0]:t,x.merge(this,x.parseHTML(r[1],t&&t.nodeType?t.ownerDocument||t:o,!0)),C.test(r[1])&&x.isPlainObject(t))for(r in t)x.isFunction(this[r])?this[r](t[r]):this.attr(r,t[r]);return this}return i=o.getElementById(r[2]),i&&i.parentNode&&(this.length=1,this[0]=i),this.context=o,this.selector=e,this}return e.nodeType?(this.context=this[0]=e,this.length=1,this):x.isFunction(e)?n.ready(e):(e.selector!==undefined&&(this.selector=e.selector,this.context=e.context),x.makeArray(e,this))},selector:"",length:0,toArray:function(){return d.call(this)},get:function(e){return null==e?this.toArray():0>e?this[this.length+e]:this[e]},pushStack:function(e){var t=x.merge(this.constructor(),e);return t.prevObject=this,t.context=this.context,t},each:function(e,t){return x.each(this,e,t)},ready:function(e){return x.ready.promise().done(e),this},slice:function(){return this.pushStack(d.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(e){var t=this.length,n=+e+(0>e?t:0);return this.pushStack(n>=0&&t>n?[this[n]]:[])},map:function(e){return this.pushStack(x.map(this,function(t,n){return e.call(t,n,t)}))},end:function(){return this.prevObject||this.constructor(null)},push:h,sort:[].sort,splice:[].splice},x.fn.init.prototype=x.fn,x.extend=x.fn.extend=function(){var e,t,n,r,i,o,s=arguments[0]||{},a=1,u=arguments.length,l=!1;for("boolean"==typeof s&&(l=s,s=arguments[1]||{},a=2),"object"==typeof s||x.isFunction(s)||(s={}),u===a&&(s=this,--a);u>a;a++)if(null!=(e=arguments[a]))for(t in e)n=s[t],r=e[t],s!==r&&(l&&r&&(x.isPlainObject(r)||(i=x.isArray(r)))?(i?(i=!1,o=n&&x.isArray(n)?n:[]):o=n&&x.isPlainObject(n)?n:{},s[t]=x.extend(l,o,r)):r!==undefined&&(s[t]=r));return s},x.extend({expando:"jQuery"+(p+Math.random()).replace(/\D/g,""),noConflict:function(t){return e.$===x&&(e.$=u),t&&e.jQuery===x&&(e.jQuery=a),x},isReady:!1,readyWait:1,holdReady:function(e){e?x.readyWait++:x.ready(!0)},ready:function(e){(e===!0?--x.readyWait:x.isReady)||(x.isReady=!0,e!==!0&&--x.readyWait>0||(n.resolveWith(o,[x]),x.fn.trigger&&x(o).trigger("ready").off("ready")))},isFunction:function(e){return"function"===x.type(e)},isArray:Array.isArray,isWindow:function(e){return null!=e&&e===e.window},isNumeric:function(e){return!isNaN(parseFloat(e))&&isFinite(e)},type:function(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?l[m.call(e)]||"object":typeof e},isPlainObject:function(e){if("object"!==x.type(e)||e.nodeType||x.isWindow(e))return!1;try{if(e.constructor&&!y.call(e.constructor.prototype,"isPrototypeOf"))return!1}catch(t){return!1}return!0},isEmptyObject:function(e){var t;for(t in e)return!1;return!0},error:function(e){throw Error(e)},parseHTML:function(e,t,n){if(!e||"string"!=typeof e)return null;"boolean"==typeof t&&(n=t,t=!1),t=t||o;var r=C.exec(e),i=!n&&[];return r?[t.createElement(r[1])]:(r=x.buildFragment([e],t,i),i&&x(i).remove(),x.merge([],r.childNodes))},parseJSON:JSON.parse,parseXML:function(e){var t,n;if(!e||"string"!=typeof e)return null;try{n=new DOMParser,t=n.parseFromString(e,"text/xml")}catch(r){t=undefined}return(!t||t.getElementsByTagName("parsererror").length)&&x.error("Invalid XML: "+e),t},noop:function(){},globalEval:function(e){var t,n=eval;e=x.trim(e),e&&(1===e.indexOf("use strict")?(t=o.createElement("script"),t.text=e,o.head.appendChild(t).parentNode.removeChild(t)):n(e))},camelCase:function(e){return e.replace(k,"ms-").replace(N,E)},nodeName:function(e,t){return e.nodeName&&e.nodeName.toLowerCase()===t.toLowerCase()},each:function(e,t,n){var r,i=0,o=e.length,s=j(e);if(n){if(s){for(;o>i;i++)if(r=t.apply(e[i],n),r===!1)break}else for(i in e)if(r=t.apply(e[i],n),r===!1)break}else if(s){for(;o>i;i++)if(r=t.call(e[i],i,e[i]),r===!1)break}else for(i in e)if(r=t.call(e[i],i,e[i]),r===!1)break;return e},trim:function(e){return null==e?"":v.call(e)},makeArray:function(e,t){var n=t||[];return null!=e&&(j(Object(e))?x.merge(n,"string"==typeof e?[e]:e):h.call(n,e)),n},inArray:function(e,t,n){return null==t?-1:g.call(t,e,n)},merge:function(e,t){var n=t.length,r=e.length,i=0;if("number"==typeof n)for(;n>i;i++)e[r++]=t[i];else while(t[i]!==undefined)e[r++]=t[i++];return e.length=r,e},grep:function(e,t,n){var r,i=[],o=0,s=e.length;for(n=!!n;s>o;o++)r=!!t(e[o],o),n!==r&&i.push(e[o]);return i},map:function(e,t,n){var r,i=0,o=e.length,s=j(e),a=[];if(s)for(;o>i;i++)r=t(e[i],i,n),null!=r&&(a[a.length]=r);else for(i in e)r=t(e[i],i,n),null!=r&&(a[a.length]=r);return f.apply([],a)},guid:1,proxy:function(e,t){var n,r,i;return"string"==typeof t&&(n=e[t],t=e,e=n),x.isFunction(e)?(r=d.call(arguments,2),i=function(){return e.apply(t||this,r.concat(d.call(arguments)))},i.guid=e.guid=e.guid||x.guid++,i):undefined},access:function(e,t,n,r,i,o,s){var a=0,u=e.length,l=null==n;if("object"===x.type(n)){i=!0;for(a in n)x.access(e,t,a,n[a],!0,o,s)}else if(r!==undefined&&(i=!0,x.isFunction(r)||(s=!0),l&&(s?(t.call(e,r),t=null):(l=t,t=function(e,t,n){return l.call(x(e),n)})),t))for(;u>a;a++)t(e[a],n,s?r:r.call(e[a],a,t(e[a],n)));return i?e:l?t.call(e):u?t(e[0],n):o},now:Date.now,swap:function(e,t,n,r){var i,o,s={};for(o in t)s[o]=e.style[o],e.style[o]=t[o];i=n.apply(e,r||[]);for(o in t)e.style[o]=s[o];return i}}),x.ready.promise=function(t){return n||(n=x.Deferred(),"complete"===o.readyState?setTimeout(x.ready):(o.addEventListener("DOMContentLoaded",S,!1),e.addEventListener("load",S,!1))),n.promise(t)},x.each("Boolean Number String Function Array Date RegExp Object Error".split(" "),function(e,t){l["[object "+t+"]"]=t.toLowerCase()});function j(e){var t=e.length,n=x.type(e);return x.isWindow(e)?!1:1===e.nodeType&&t?!0:"array"===n||"function"!==n&&(0===t||"number"==typeof t&&t>0&&t-1 in e)}t=x(o),function(e,undefined){var t,n,r,i,o,s,a,u,l,c,p,f,h,d,g,m,y,v="sizzle"+-new Date,b=e.document,w=0,T=0,C=st(),k=st(),N=st(),E=!1,S=function(e,t){return e===t?(E=!0,0):0},j=typeof undefined,D=1<<31,A={}.hasOwnProperty,L=[],q=L.pop,H=L.push,O=L.push,F=L.slice,P=L.indexOf||function(e){var t=0,n=this.length;for(;n>t;t++)if(this[t]===e)return t;return-1},R="checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped",M="[\\x20\\t\\r\\n\\f]",W="(?:\\\\.|[\\w-]|[^\\x00-\\xa0])+",$=W.replace("w","w#"),B="\\["+M+"*("+W+")"+M+"*(?:([*^$|!~]?=)"+M+"*(?:(['\"])((?:\\\\.|[^\\\\])*?)\\3|("+$+")|)|)"+M+"*\\]",I=":("+W+")(?:\\(((['\"])((?:\\\\.|[^\\\\])*?)\\3|((?:\\\\.|[^\\\\()[\\]]|"+B.replace(3,8)+")*)|.*)\\)|)",z=RegExp("^"+M+"+|((?:^|[^\\\\])(?:\\\\.)*)"+M+"+$","g"),_=RegExp("^"+M+"*,"+M+"*"),X=RegExp("^"+M+"*([>+~]|"+M+")"+M+"*"),U=RegExp(M+"*[+~]"),Y=RegExp("="+M+"*([^\\]'\"]*)"+M+"*\\]","g"),V=RegExp(I),G=RegExp("^"+$+"$"),J={ID:RegExp("^#("+W+")"),CLASS:RegExp("^\\.("+W+")"),TAG:RegExp("^("+W.replace("w","w*")+")"),ATTR:RegExp("^"+B),PSEUDO:RegExp("^"+I),CHILD:RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+M+"*(even|odd|(([+-]|)(\\d*)n|)"+M+"*(?:([+-]|)"+M+"*(\\d+)|))"+M+"*\\)|)","i"),bool:RegExp("^(?:"+R+")$","i"),needsContext:RegExp("^"+M+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+M+"*((?:-\\d)?\\d*)"+M+"*\\)|)(?=[^-]|$)","i")},Q=/^[^{]+\{\s*\[native \w/,K=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,Z=/^(?:input|select|textarea|button)$/i,et=/^h\d$/i,tt=/'|\\/g,nt=RegExp("\\\\([\\da-f]{1,6}"+M+"?|("+M+")|.)","ig"),rt=function(e,t,n){var r="0x"+t-65536;return r!==r||n?t:0>r?String.fromCharCode(r+65536):String.fromCharCode(55296|r>>10,56320|1023&r)};try{O.apply(L=F.call(b.childNodes),b.childNodes),L[b.childNodes.length].nodeType}catch(it){O={apply:L.length?function(e,t){H.apply(e,F.call(t))}:function(e,t){var n=e.length,r=0;while(e[n++]=t[r++]);e.length=n-1}}}function ot(e,t,r,i){var o,s,a,u,l,f,g,m,x,w;if((t?t.ownerDocument||t:b)!==p&&c(t),t=t||p,r=r||[],!e||"string"!=typeof e)return r;if(1!==(u=t.nodeType)&&9!==u)return[];if(h&&!i){if(o=K.exec(e))if(a=o[1]){if(9===u){if(s=t.getElementById(a),!s||!s.parentNode)return r;if(s.id===a)return r.push(s),r}else if(t.ownerDocument&&(s=t.ownerDocument.getElementById(a))&&y(t,s)&&s.id===a)return r.push(s),r}else{if(o[2])return O.apply(r,t.getElementsByTagName(e)),r;if((a=o[3])&&n.getElementsByClassName&&t.getElementsByClassName)return O.apply(r,t.getElementsByClassName(a)),r}if(n.qsa&&(!d||!d.test(e))){if(m=g=v,x=t,w=9===u&&e,1===u&&"object"!==t.nodeName.toLowerCase()){f=gt(e),(g=t.getAttribute("id"))?m=g.replace(tt,"\\$&"):t.setAttribute("id",m),m="[id='"+m+"'] ",l=f.length;while(l--)f[l]=m+mt(f[l]);x=U.test(e)&&t.parentNode||t,w=f.join(",")}if(w)try{return O.apply(r,x.querySelectorAll(w)),r}catch(T){}finally{g||t.removeAttribute("id")}}}return kt(e.replace(z,"$1"),t,r,i)}function st(){var e=[];function t(n,r){return e.push(n+=" ")>i.cacheLength&&delete t[e.shift()],t[n]=r}return t}function at(e){return e[v]=!0,e}function ut(e){var t=p.createElement("div");try{return!!e(t)}catch(n){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function lt(e,t){var n=e.split("|"),r=e.length;while(r--)i.attrHandle[n[r]]=t}function ct(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&(~t.sourceIndex||D)-(~e.sourceIndex||D);if(r)return r;if(n)while(n=n.nextSibling)if(n===t)return-1;return e?1:-1}function pt(e){return function(t){var n=t.nodeName.toLowerCase();return"input"===n&&t.type===e}}function ft(e){return function(t){var n=t.nodeName.toLowerCase();return("input"===n||"button"===n)&&t.type===e}}function ht(e){return at(function(t){return t=+t,at(function(n,r){var i,o=e([],n.length,t),s=o.length;while(s--)n[i=o[s]]&&(n[i]=!(r[i]=n[i]))})})}s=ot.isXML=function(e){var t=e&&(e.ownerDocument||e).documentElement;return t?"HTML"!==t.nodeName:!1},n=ot.support={},c=ot.setDocument=function(e){var t=e?e.ownerDocument||e:b,r=t.defaultView;return t!==p&&9===t.nodeType&&t.documentElement?(p=t,f=t.documentElement,h=!s(t),r&&r.attachEvent&&r!==r.top&&r.attachEvent("onbeforeunload",function(){c()}),n.attributes=ut(function(e){return e.className="i",!e.getAttribute("className")}),n.getElementsByTagName=ut(function(e){return e.appendChild(t.createComment("")),!e.getElementsByTagName("*").length}),n.getElementsByClassName=ut(function(e){return e.innerHTML="
          ",e.firstChild.className="i",2===e.getElementsByClassName("i").length}),n.getById=ut(function(e){return f.appendChild(e).id=v,!t.getElementsByName||!t.getElementsByName(v).length}),n.getById?(i.find.ID=function(e,t){if(typeof t.getElementById!==j&&h){var n=t.getElementById(e);return n&&n.parentNode?[n]:[]}},i.filter.ID=function(e){var t=e.replace(nt,rt);return function(e){return e.getAttribute("id")===t}}):(delete i.find.ID,i.filter.ID=function(e){var t=e.replace(nt,rt);return function(e){var n=typeof e.getAttributeNode!==j&&e.getAttributeNode("id");return n&&n.value===t}}),i.find.TAG=n.getElementsByTagName?function(e,t){return typeof t.getElementsByTagName!==j?t.getElementsByTagName(e):undefined}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"===e){while(n=o[i++])1===n.nodeType&&r.push(n);return r}return o},i.find.CLASS=n.getElementsByClassName&&function(e,t){return typeof t.getElementsByClassName!==j&&h?t.getElementsByClassName(e):undefined},g=[],d=[],(n.qsa=Q.test(t.querySelectorAll))&&(ut(function(e){e.innerHTML="",e.querySelectorAll("[selected]").length||d.push("\\["+M+"*(?:value|"+R+")"),e.querySelectorAll(":checked").length||d.push(":checked")}),ut(function(e){var n=t.createElement("input");n.setAttribute("type","hidden"),e.appendChild(n).setAttribute("t",""),e.querySelectorAll("[t^='']").length&&d.push("[*^$]="+M+"*(?:''|\"\")"),e.querySelectorAll(":enabled").length||d.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),d.push(",.*:")})),(n.matchesSelector=Q.test(m=f.webkitMatchesSelector||f.mozMatchesSelector||f.oMatchesSelector||f.msMatchesSelector))&&ut(function(e){n.disconnectedMatch=m.call(e,"div"),m.call(e,"[s!='']:x"),g.push("!=",I)}),d=d.length&&RegExp(d.join("|")),g=g.length&&RegExp(g.join("|")),y=Q.test(f.contains)||f.compareDocumentPosition?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)while(t=t.parentNode)if(t===e)return!0;return!1},S=f.compareDocumentPosition?function(e,r){if(e===r)return E=!0,0;var i=r.compareDocumentPosition&&e.compareDocumentPosition&&e.compareDocumentPosition(r);return i?1&i||!n.sortDetached&&r.compareDocumentPosition(e)===i?e===t||y(b,e)?-1:r===t||y(b,r)?1:l?P.call(l,e)-P.call(l,r):0:4&i?-1:1:e.compareDocumentPosition?-1:1}:function(e,n){var r,i=0,o=e.parentNode,s=n.parentNode,a=[e],u=[n];if(e===n)return E=!0,0;if(!o||!s)return e===t?-1:n===t?1:o?-1:s?1:l?P.call(l,e)-P.call(l,n):0;if(o===s)return ct(e,n);r=e;while(r=r.parentNode)a.unshift(r);r=n;while(r=r.parentNode)u.unshift(r);while(a[i]===u[i])i++;return i?ct(a[i],u[i]):a[i]===b?-1:u[i]===b?1:0},t):p},ot.matches=function(e,t){return ot(e,null,null,t)},ot.matchesSelector=function(e,t){if((e.ownerDocument||e)!==p&&c(e),t=t.replace(Y,"='$1']"),!(!n.matchesSelector||!h||g&&g.test(t)||d&&d.test(t)))try{var r=m.call(e,t);if(r||n.disconnectedMatch||e.document&&11!==e.document.nodeType)return r}catch(i){}return ot(t,p,null,[e]).length>0},ot.contains=function(e,t){return(e.ownerDocument||e)!==p&&c(e),y(e,t)},ot.attr=function(e,t){(e.ownerDocument||e)!==p&&c(e);var r=i.attrHandle[t.toLowerCase()],o=r&&A.call(i.attrHandle,t.toLowerCase())?r(e,t,!h):undefined;return o===undefined?n.attributes||!h?e.getAttribute(t):(o=e.getAttributeNode(t))&&o.specified?o.value:null:o},ot.error=function(e){throw Error("Syntax error, unrecognized expression: "+e)},ot.uniqueSort=function(e){var t,r=[],i=0,o=0;if(E=!n.detectDuplicates,l=!n.sortStable&&e.slice(0),e.sort(S),E){while(t=e[o++])t===e[o]&&(i=r.push(o));while(i--)e.splice(r[i],1)}return e},o=ot.getText=function(e){var t,n="",r=0,i=e.nodeType;if(i){if(1===i||9===i||11===i){if("string"==typeof e.textContent)return e.textContent;for(e=e.firstChild;e;e=e.nextSibling)n+=o(e)}else if(3===i||4===i)return e.nodeValue}else for(;t=e[r];r++)n+=o(t);return n},i=ot.selectors={cacheLength:50,createPseudo:at,match:J,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(nt,rt),e[3]=(e[4]||e[5]||"").replace(nt,rt),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||ot.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&ot.error(e[0]),e},PSEUDO:function(e){var t,n=!e[5]&&e[2];return J.CHILD.test(e[0])?null:(e[3]&&e[4]!==undefined?e[2]=e[4]:n&&V.test(n)&&(t=gt(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(nt,rt).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=C[e+" "];return t||(t=RegExp("(^|"+M+")"+e+"("+M+"|$)"))&&C(e,function(e){return t.test("string"==typeof e.className&&e.className||typeof e.getAttribute!==j&&e.getAttribute("class")||"")})},ATTR:function(e,t,n){return function(r){var i=ot.attr(r,e);return null==i?"!="===t:t?(i+="","="===t?i===n:"!="===t?i!==n:"^="===t?n&&0===i.indexOf(n):"*="===t?n&&i.indexOf(n)>-1:"$="===t?n&&i.slice(-n.length)===n:"~="===t?(" "+i+" ").indexOf(n)>-1:"|="===t?i===n||i.slice(0,n.length+1)===n+"-":!1):!0}},CHILD:function(e,t,n,r,i){var o="nth"!==e.slice(0,3),s="last"!==e.slice(-4),a="of-type"===t;return 1===r&&0===i?function(e){return!!e.parentNode}:function(t,n,u){var l,c,p,f,h,d,g=o!==s?"nextSibling":"previousSibling",m=t.parentNode,y=a&&t.nodeName.toLowerCase(),x=!u&&!a;if(m){if(o){while(g){p=t;while(p=p[g])if(a?p.nodeName.toLowerCase()===y:1===p.nodeType)return!1;d=g="only"===e&&!d&&"nextSibling"}return!0}if(d=[s?m.firstChild:m.lastChild],s&&x){c=m[v]||(m[v]={}),l=c[e]||[],h=l[0]===w&&l[1],f=l[0]===w&&l[2],p=h&&m.childNodes[h];while(p=++h&&p&&p[g]||(f=h=0)||d.pop())if(1===p.nodeType&&++f&&p===t){c[e]=[w,h,f];break}}else if(x&&(l=(t[v]||(t[v]={}))[e])&&l[0]===w)f=l[1];else while(p=++h&&p&&p[g]||(f=h=0)||d.pop())if((a?p.nodeName.toLowerCase()===y:1===p.nodeType)&&++f&&(x&&((p[v]||(p[v]={}))[e]=[w,f]),p===t))break;return f-=i,f===r||0===f%r&&f/r>=0}}},PSEUDO:function(e,t){var n,r=i.pseudos[e]||i.setFilters[e.toLowerCase()]||ot.error("unsupported pseudo: "+e);return r[v]?r(t):r.length>1?(n=[e,e,"",t],i.setFilters.hasOwnProperty(e.toLowerCase())?at(function(e,n){var i,o=r(e,t),s=o.length;while(s--)i=P.call(e,o[s]),e[i]=!(n[i]=o[s])}):function(e){return r(e,0,n)}):r}},pseudos:{not:at(function(e){var t=[],n=[],r=a(e.replace(z,"$1"));return r[v]?at(function(e,t,n,i){var o,s=r(e,null,i,[]),a=e.length;while(a--)(o=s[a])&&(e[a]=!(t[a]=o))}):function(e,i,o){return t[0]=e,r(t,null,o,n),!n.pop()}}),has:at(function(e){return function(t){return ot(e,t).length>0}}),contains:at(function(e){return function(t){return(t.textContent||t.innerText||o(t)).indexOf(e)>-1}}),lang:at(function(e){return G.test(e||"")||ot.error("unsupported lang: "+e),e=e.replace(nt,rt).toLowerCase(),function(t){var n;do if(n=h?t.lang:t.getAttribute("xml:lang")||t.getAttribute("lang"))return n=n.toLowerCase(),n===e||0===n.indexOf(e+"-");while((t=t.parentNode)&&1===t.nodeType);return!1}}),target:function(t){var n=e.location&&e.location.hash;return n&&n.slice(1)===t.id},root:function(e){return e===f},focus:function(e){return e===p.activeElement&&(!p.hasFocus||p.hasFocus())&&!!(e.type||e.href||~e.tabIndex)},enabled:function(e){return e.disabled===!1},disabled:function(e){return e.disabled===!0},checked:function(e){var t=e.nodeName.toLowerCase();return"input"===t&&!!e.checked||"option"===t&&!!e.selected},selected:function(e){return e.parentNode&&e.parentNode.selectedIndex,e.selected===!0},empty:function(e){for(e=e.firstChild;e;e=e.nextSibling)if(e.nodeName>"@"||3===e.nodeType||4===e.nodeType)return!1;return!0},parent:function(e){return!i.pseudos.empty(e)},header:function(e){return et.test(e.nodeName)},input:function(e){return Z.test(e.nodeName)},button:function(e){var t=e.nodeName.toLowerCase();return"input"===t&&"button"===e.type||"button"===t},text:function(e){var t;return"input"===e.nodeName.toLowerCase()&&"text"===e.type&&(null==(t=e.getAttribute("type"))||t.toLowerCase()===e.type)},first:ht(function(){return[0]}),last:ht(function(e,t){return[t-1]}),eq:ht(function(e,t,n){return[0>n?n+t:n]}),even:ht(function(e,t){var n=0;for(;t>n;n+=2)e.push(n);return e}),odd:ht(function(e,t){var n=1;for(;t>n;n+=2)e.push(n);return e}),lt:ht(function(e,t,n){var r=0>n?n+t:n;for(;--r>=0;)e.push(r);return e}),gt:ht(function(e,t,n){var r=0>n?n+t:n;for(;t>++r;)e.push(r);return e})}},i.pseudos.nth=i.pseudos.eq;for(t in{radio:!0,checkbox:!0,file:!0,password:!0,image:!0})i.pseudos[t]=pt(t);for(t in{submit:!0,reset:!0})i.pseudos[t]=ft(t);function dt(){}dt.prototype=i.filters=i.pseudos,i.setFilters=new dt;function gt(e,t){var n,r,o,s,a,u,l,c=k[e+" "];if(c)return t?0:c.slice(0);a=e,u=[],l=i.preFilter;while(a){(!n||(r=_.exec(a)))&&(r&&(a=a.slice(r[0].length)||a),u.push(o=[])),n=!1,(r=X.exec(a))&&(n=r.shift(),o.push({value:n,type:r[0].replace(z," ")}),a=a.slice(n.length));for(s in i.filter)!(r=J[s].exec(a))||l[s]&&!(r=l[s](r))||(n=r.shift(),o.push({value:n,type:s,matches:r}),a=a.slice(n.length));if(!n)break}return t?a.length:a?ot.error(e):k(e,u).slice(0)}function mt(e){var t=0,n=e.length,r="";for(;n>t;t++)r+=e[t].value;return r}function yt(e,t,n){var i=t.dir,o=n&&"parentNode"===i,s=T++;return t.first?function(t,n,r){while(t=t[i])if(1===t.nodeType||o)return e(t,n,r)}:function(t,n,a){var u,l,c,p=w+" "+s;if(a){while(t=t[i])if((1===t.nodeType||o)&&e(t,n,a))return!0}else while(t=t[i])if(1===t.nodeType||o)if(c=t[v]||(t[v]={}),(l=c[i])&&l[0]===p){if((u=l[1])===!0||u===r)return u===!0}else if(l=c[i]=[p],l[1]=e(t,n,a)||r,l[1]===!0)return!0}}function vt(e){return e.length>1?function(t,n,r){var i=e.length;while(i--)if(!e[i](t,n,r))return!1;return!0}:e[0]}function xt(e,t,n,r,i){var o,s=[],a=0,u=e.length,l=null!=t;for(;u>a;a++)(o=e[a])&&(!n||n(o,r,i))&&(s.push(o),l&&t.push(a));return s}function bt(e,t,n,r,i,o){return r&&!r[v]&&(r=bt(r)),i&&!i[v]&&(i=bt(i,o)),at(function(o,s,a,u){var l,c,p,f=[],h=[],d=s.length,g=o||Ct(t||"*",a.nodeType?[a]:a,[]),m=!e||!o&&t?g:xt(g,f,e,a,u),y=n?i||(o?e:d||r)?[]:s:m;if(n&&n(m,y,a,u),r){l=xt(y,h),r(l,[],a,u),c=l.length;while(c--)(p=l[c])&&(y[h[c]]=!(m[h[c]]=p))}if(o){if(i||e){if(i){l=[],c=y.length;while(c--)(p=y[c])&&l.push(m[c]=p);i(null,y=[],l,u)}c=y.length;while(c--)(p=y[c])&&(l=i?P.call(o,p):f[c])>-1&&(o[l]=!(s[l]=p))}}else y=xt(y===s?y.splice(d,y.length):y),i?i(null,s,y,u):O.apply(s,y)})}function wt(e){var t,n,r,o=e.length,s=i.relative[e[0].type],a=s||i.relative[" "],l=s?1:0,c=yt(function(e){return e===t},a,!0),p=yt(function(e){return P.call(t,e)>-1},a,!0),f=[function(e,n,r){return!s&&(r||n!==u)||((t=n).nodeType?c(e,n,r):p(e,n,r))}];for(;o>l;l++)if(n=i.relative[e[l].type])f=[yt(vt(f),n)];else{if(n=i.filter[e[l].type].apply(null,e[l].matches),n[v]){for(r=++l;o>r;r++)if(i.relative[e[r].type])break;return bt(l>1&&vt(f),l>1&&mt(e.slice(0,l-1).concat({value:" "===e[l-2].type?"*":""})).replace(z,"$1"),n,r>l&&wt(e.slice(l,r)),o>r&&wt(e=e.slice(r)),o>r&&mt(e))}f.push(n)}return vt(f)}function Tt(e,t){var n=0,o=t.length>0,s=e.length>0,a=function(a,l,c,f,h){var d,g,m,y=[],v=0,x="0",b=a&&[],T=null!=h,C=u,k=a||s&&i.find.TAG("*",h&&l.parentNode||l),N=w+=null==C?1:Math.random()||.1;for(T&&(u=l!==p&&l,r=n);null!=(d=k[x]);x++){if(s&&d){g=0;while(m=e[g++])if(m(d,l,c)){f.push(d);break}T&&(w=N,r=++n)}o&&((d=!m&&d)&&v--,a&&b.push(d))}if(v+=x,o&&x!==v){g=0;while(m=t[g++])m(b,y,l,c);if(a){if(v>0)while(x--)b[x]||y[x]||(y[x]=q.call(f));y=xt(y)}O.apply(f,y),T&&!a&&y.length>0&&v+t.length>1&&ot.uniqueSort(f)}return T&&(w=N,u=C),b};return o?at(a):a}a=ot.compile=function(e,t){var n,r=[],i=[],o=N[e+" "];if(!o){t||(t=gt(e)),n=t.length;while(n--)o=wt(t[n]),o[v]?r.push(o):i.push(o);o=N(e,Tt(i,r))}return o};function Ct(e,t,n){var r=0,i=t.length;for(;i>r;r++)ot(e,t[r],n);return n}function kt(e,t,r,o){var s,u,l,c,p,f=gt(e);if(!o&&1===f.length){if(u=f[0]=f[0].slice(0),u.length>2&&"ID"===(l=u[0]).type&&n.getById&&9===t.nodeType&&h&&i.relative[u[1].type]){if(t=(i.find.ID(l.matches[0].replace(nt,rt),t)||[])[0],!t)return r;e=e.slice(u.shift().value.length)}s=J.needsContext.test(e)?0:u.length;while(s--){if(l=u[s],i.relative[c=l.type])break;if((p=i.find[c])&&(o=p(l.matches[0].replace(nt,rt),U.test(u[0].type)&&t.parentNode||t))){if(u.splice(s,1),e=o.length&&mt(u),!e)return O.apply(r,o),r;break}}}return a(e,f)(o,t,!h,r,U.test(e)),r}n.sortStable=v.split("").sort(S).join("")===v,n.detectDuplicates=E,c(),n.sortDetached=ut(function(e){return 1&e.compareDocumentPosition(p.createElement("div"))}),ut(function(e){return e.innerHTML="
          ","#"===e.firstChild.getAttribute("href")})||lt("type|href|height|width",function(e,t,n){return n?undefined:e.getAttribute(t,"type"===t.toLowerCase()?1:2)}),n.attributes&&ut(function(e){return e.innerHTML="",e.firstChild.setAttribute("value",""),""===e.firstChild.getAttribute("value")})||lt("value",function(e,t,n){return n||"input"!==e.nodeName.toLowerCase()?undefined:e.defaultValue}),ut(function(e){return null==e.getAttribute("disabled")})||lt(R,function(e,t,n){var r;return n?undefined:(r=e.getAttributeNode(t))&&r.specified?r.value:e[t]===!0?t.toLowerCase():null}),x.find=ot,x.expr=ot.selectors,x.expr[":"]=x.expr.pseudos,x.unique=ot.uniqueSort,x.text=ot.getText,x.isXMLDoc=ot.isXML,x.contains=ot.contains}(e);var D={};function A(e){var t=D[e]={};return x.each(e.match(w)||[],function(e,n){t[n]=!0}),t}x.Callbacks=function(e){e="string"==typeof e?D[e]||A(e):x.extend({},e);var t,n,r,i,o,s,a=[],u=!e.once&&[],l=function(p){for(t=e.memory&&p,n=!0,s=i||0,i=0,o=a.length,r=!0;a&&o>s;s++)if(a[s].apply(p[0],p[1])===!1&&e.stopOnFalse){t=!1;break}r=!1,a&&(u?u.length&&l(u.shift()):t?a=[]:c.disable())},c={add:function(){if(a){var n=a.length;(function s(t){x.each(t,function(t,n){var r=x.type(n);"function"===r?e.unique&&c.has(n)||a.push(n):n&&n.length&&"string"!==r&&s(n)})})(arguments),r?o=a.length:t&&(i=n,l(t))}return this},remove:function(){return a&&x.each(arguments,function(e,t){var n;while((n=x.inArray(t,a,n))>-1)a.splice(n,1),r&&(o>=n&&o--,s>=n&&s--)}),this},has:function(e){return e?x.inArray(e,a)>-1:!(!a||!a.length)},empty:function(){return a=[],o=0,this},disable:function(){return a=u=t=undefined,this},disabled:function(){return!a},lock:function(){return u=undefined,t||c.disable(),this},locked:function(){return!u},fireWith:function(e,t){return!a||n&&!u||(t=t||[],t=[e,t.slice?t.slice():t],r?u.push(t):l(t)),this},fire:function(){return c.fireWith(this,arguments),this},fired:function(){return!!n}};return c},x.extend({Deferred:function(e){var t=[["resolve","done",x.Callbacks("once memory"),"resolved"],["reject","fail",x.Callbacks("once memory"),"rejected"],["notify","progress",x.Callbacks("memory")]],n="pending",r={state:function(){return n},always:function(){return i.done(arguments).fail(arguments),this},then:function(){var e=arguments;return x.Deferred(function(n){x.each(t,function(t,o){var s=o[0],a=x.isFunction(e[t])&&e[t];i[o[1]](function(){var e=a&&a.apply(this,arguments);e&&x.isFunction(e.promise)?e.promise().done(n.resolve).fail(n.reject).progress(n.notify):n[s+"With"](this===r?n.promise():this,a?[e]:arguments)})}),e=null}).promise()},promise:function(e){return null!=e?x.extend(e,r):r}},i={};return r.pipe=r.then,x.each(t,function(e,o){var s=o[2],a=o[3];r[o[1]]=s.add,a&&s.add(function(){n=a},t[1^e][2].disable,t[2][2].lock),i[o[0]]=function(){return i[o[0]+"With"](this===i?r:this,arguments),this},i[o[0]+"With"]=s.fireWith}),r.promise(i),e&&e.call(i,i),i},when:function(e){var t=0,n=d.call(arguments),r=n.length,i=1!==r||e&&x.isFunction(e.promise)?r:0,o=1===i?e:x.Deferred(),s=function(e,t,n){return function(r){t[e]=this,n[e]=arguments.length>1?d.call(arguments):r,n===a?o.notifyWith(t,n):--i||o.resolveWith(t,n)}},a,u,l;if(r>1)for(a=Array(r),u=Array(r),l=Array(r);r>t;t++)n[t]&&x.isFunction(n[t].promise)?n[t].promise().done(s(t,l,n)).fail(o.reject).progress(s(t,u,a)):--i;return i||o.resolveWith(l,n),o.promise()}}),x.support=function(t){var n=o.createElement("input"),r=o.createDocumentFragment(),i=o.createElement("div"),s=o.createElement("select"),a=s.appendChild(o.createElement("option"));return n.type?(n.type="checkbox",t.checkOn=""!==n.value,t.optSelected=a.selected,t.reliableMarginRight=!0,t.boxSizingReliable=!0,t.pixelPosition=!1,n.checked=!0,t.noCloneChecked=n.cloneNode(!0).checked,s.disabled=!0,t.optDisabled=!a.disabled,n=o.createElement("input"),n.value="t",n.type="radio",t.radioValue="t"===n.value,n.setAttribute("checked","t"),n.setAttribute("name","t"),r.appendChild(n),t.checkClone=r.cloneNode(!0).cloneNode(!0).lastChild.checked,t.focusinBubbles="onfocusin"in e,i.style.backgroundClip="content-box",i.cloneNode(!0).style.backgroundClip="",t.clearCloneStyle="content-box"===i.style.backgroundClip,x(function(){var n,r,s="padding:0;margin:0;border:0;display:block;-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box",a=o.getElementsByTagName("body")[0];a&&(n=o.createElement("div"),n.style.cssText="border:0;width:0;height:0;position:absolute;top:0;left:-9999px;margin-top:1px",a.appendChild(n).appendChild(i),i.innerHTML="",i.style.cssText="-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;padding:1px;border:1px;display:block;width:4px;margin-top:1%;position:absolute;top:1%",x.swap(a,null!=a.style.zoom?{zoom:1}:{},function(){t.boxSizing=4===i.offsetWidth}),e.getComputedStyle&&(t.pixelPosition="1%"!==(e.getComputedStyle(i,null)||{}).top,t.boxSizingReliable="4px"===(e.getComputedStyle(i,null)||{width:"4px"}).width,r=i.appendChild(o.createElement("div")),r.style.cssText=i.style.cssText=s,r.style.marginRight=r.style.width="0",i.style.width="1px",t.reliableMarginRight=!parseFloat((e.getComputedStyle(r,null)||{}).marginRight)),a.removeChild(n))}),t):t}({});var L,q,H=/(?:\{[\s\S]*\}|\[[\s\S]*\])$/,O=/([A-Z])/g;function F(){Object.defineProperty(this.cache={},0,{get:function(){return{}}}),this.expando=x.expando+Math.random()}F.uid=1,F.accepts=function(e){return e.nodeType?1===e.nodeType||9===e.nodeType:!0},F.prototype={key:function(e){if(!F.accepts(e))return 0;var t={},n=e[this.expando];if(!n){n=F.uid++;try{t[this.expando]={value:n},Object.defineProperties(e,t)}catch(r){t[this.expando]=n,x.extend(e,t)}}return this.cache[n]||(this.cache[n]={}),n},set:function(e,t,n){var r,i=this.key(e),o=this.cache[i];if("string"==typeof t)o[t]=n;else if(x.isEmptyObject(o))x.extend(this.cache[i],t);else for(r in t)o[r]=t[r];return o},get:function(e,t){var n=this.cache[this.key(e)];return t===undefined?n:n[t]},access:function(e,t,n){var r;return t===undefined||t&&"string"==typeof t&&n===undefined?(r=this.get(e,t),r!==undefined?r:this.get(e,x.camelCase(t))):(this.set(e,t,n),n!==undefined?n:t)},remove:function(e,t){var n,r,i,o=this.key(e),s=this.cache[o];if(t===undefined)this.cache[o]={};else{x.isArray(t)?r=t.concat(t.map(x.camelCase)):(i=x.camelCase(t),t in s?r=[t,i]:(r=i,r=r in s?[r]:r.match(w)||[])),n=r.length;while(n--)delete s[r[n]]}},hasData:function(e){return!x.isEmptyObject(this.cache[e[this.expando]]||{})},discard:function(e){e[this.expando]&&delete this.cache[e[this.expando]]}},L=new F,q=new F,x.extend({acceptData:F.accepts,hasData:function(e){return L.hasData(e)||q.hasData(e)},data:function(e,t,n){return L.access(e,t,n)},removeData:function(e,t){L.remove(e,t)},_data:function(e,t,n){return q.access(e,t,n)},_removeData:function(e,t){q.remove(e,t)}}),x.fn.extend({data:function(e,t){var n,r,i=this[0],o=0,s=null;if(e===undefined){if(this.length&&(s=L.get(i),1===i.nodeType&&!q.get(i,"hasDataAttrs"))){for(n=i.attributes;n.length>o;o++)r=n[o].name,0===r.indexOf("data-")&&(r=x.camelCase(r.slice(5)),P(i,r,s[r]));q.set(i,"hasDataAttrs",!0)}return s}return"object"==typeof e?this.each(function(){L.set(this,e)}):x.access(this,function(t){var n,r=x.camelCase(e);if(i&&t===undefined){if(n=L.get(i,e),n!==undefined)return n;if(n=L.get(i,r),n!==undefined)return n;if(n=P(i,r,undefined),n!==undefined)return n}else this.each(function(){var n=L.get(this,r);L.set(this,r,t),-1!==e.indexOf("-")&&n!==undefined&&L.set(this,e,t)})},null,t,arguments.length>1,null,!0)},removeData:function(e){return this.each(function(){L.remove(this,e)})}});function P(e,t,n){var r;if(n===undefined&&1===e.nodeType)if(r="data-"+t.replace(O,"-$1").toLowerCase(),n=e.getAttribute(r),"string"==typeof n){try{n="true"===n?!0:"false"===n?!1:"null"===n?null:+n+""===n?+n:H.test(n)?JSON.parse(n):n}catch(i){}L.set(e,t,n)}else n=undefined;return n}x.extend({queue:function(e,t,n){var r;return e?(t=(t||"fx")+"queue",r=q.get(e,t),n&&(!r||x.isArray(n)?r=q.access(e,t,x.makeArray(n)):r.push(n)),r||[]):undefined},dequeue:function(e,t){t=t||"fx";var n=x.queue(e,t),r=n.length,i=n.shift(),o=x._queueHooks(e,t),s=function(){x.dequeue(e,t) -};"inprogress"===i&&(i=n.shift(),r--),i&&("fx"===t&&n.unshift("inprogress"),delete o.stop,i.call(e,s,o)),!r&&o&&o.empty.fire()},_queueHooks:function(e,t){var n=t+"queueHooks";return q.get(e,n)||q.access(e,n,{empty:x.Callbacks("once memory").add(function(){q.remove(e,[t+"queue",n])})})}}),x.fn.extend({queue:function(e,t){var n=2;return"string"!=typeof e&&(t=e,e="fx",n--),n>arguments.length?x.queue(this[0],e):t===undefined?this:this.each(function(){var n=x.queue(this,e,t);x._queueHooks(this,e),"fx"===e&&"inprogress"!==n[0]&&x.dequeue(this,e)})},dequeue:function(e){return this.each(function(){x.dequeue(this,e)})},delay:function(e,t){return e=x.fx?x.fx.speeds[e]||e:e,t=t||"fx",this.queue(t,function(t,n){var r=setTimeout(t,e);n.stop=function(){clearTimeout(r)}})},clearQueue:function(e){return this.queue(e||"fx",[])},promise:function(e,t){var n,r=1,i=x.Deferred(),o=this,s=this.length,a=function(){--r||i.resolveWith(o,[o])};"string"!=typeof e&&(t=e,e=undefined),e=e||"fx";while(s--)n=q.get(o[s],e+"queueHooks"),n&&n.empty&&(r++,n.empty.add(a));return a(),i.promise(t)}});var R,M,W=/[\t\r\n\f]/g,$=/\r/g,B=/^(?:input|select|textarea|button)$/i;x.fn.extend({attr:function(e,t){return x.access(this,x.attr,e,t,arguments.length>1)},removeAttr:function(e){return this.each(function(){x.removeAttr(this,e)})},prop:function(e,t){return x.access(this,x.prop,e,t,arguments.length>1)},removeProp:function(e){return this.each(function(){delete this[x.propFix[e]||e]})},addClass:function(e){var t,n,r,i,o,s=0,a=this.length,u="string"==typeof e&&e;if(x.isFunction(e))return this.each(function(t){x(this).addClass(e.call(this,t,this.className))});if(u)for(t=(e||"").match(w)||[];a>s;s++)if(n=this[s],r=1===n.nodeType&&(n.className?(" "+n.className+" ").replace(W," "):" ")){o=0;while(i=t[o++])0>r.indexOf(" "+i+" ")&&(r+=i+" ");n.className=x.trim(r)}return this},removeClass:function(e){var t,n,r,i,o,s=0,a=this.length,u=0===arguments.length||"string"==typeof e&&e;if(x.isFunction(e))return this.each(function(t){x(this).removeClass(e.call(this,t,this.className))});if(u)for(t=(e||"").match(w)||[];a>s;s++)if(n=this[s],r=1===n.nodeType&&(n.className?(" "+n.className+" ").replace(W," "):"")){o=0;while(i=t[o++])while(r.indexOf(" "+i+" ")>=0)r=r.replace(" "+i+" "," ");n.className=e?x.trim(r):""}return this},toggleClass:function(e,t){var n=typeof e;return"boolean"==typeof t&&"string"===n?t?this.addClass(e):this.removeClass(e):x.isFunction(e)?this.each(function(n){x(this).toggleClass(e.call(this,n,this.className,t),t)}):this.each(function(){if("string"===n){var t,i=0,o=x(this),s=e.match(w)||[];while(t=s[i++])o.hasClass(t)?o.removeClass(t):o.addClass(t)}else(n===r||"boolean"===n)&&(this.className&&q.set(this,"__className__",this.className),this.className=this.className||e===!1?"":q.get(this,"__className__")||"")})},hasClass:function(e){var t=" "+e+" ",n=0,r=this.length;for(;r>n;n++)if(1===this[n].nodeType&&(" "+this[n].className+" ").replace(W," ").indexOf(t)>=0)return!0;return!1},val:function(e){var t,n,r,i=this[0];{if(arguments.length)return r=x.isFunction(e),this.each(function(n){var i;1===this.nodeType&&(i=r?e.call(this,n,x(this).val()):e,null==i?i="":"number"==typeof i?i+="":x.isArray(i)&&(i=x.map(i,function(e){return null==e?"":e+""})),t=x.valHooks[this.type]||x.valHooks[this.nodeName.toLowerCase()],t&&"set"in t&&t.set(this,i,"value")!==undefined||(this.value=i))});if(i)return t=x.valHooks[i.type]||x.valHooks[i.nodeName.toLowerCase()],t&&"get"in t&&(n=t.get(i,"value"))!==undefined?n:(n=i.value,"string"==typeof n?n.replace($,""):null==n?"":n)}}}),x.extend({valHooks:{option:{get:function(e){var t=e.attributes.value;return!t||t.specified?e.value:e.text}},select:{get:function(e){var t,n,r=e.options,i=e.selectedIndex,o="select-one"===e.type||0>i,s=o?null:[],a=o?i+1:r.length,u=0>i?a:o?i:0;for(;a>u;u++)if(n=r[u],!(!n.selected&&u!==i||(x.support.optDisabled?n.disabled:null!==n.getAttribute("disabled"))||n.parentNode.disabled&&x.nodeName(n.parentNode,"optgroup"))){if(t=x(n).val(),o)return t;s.push(t)}return s},set:function(e,t){var n,r,i=e.options,o=x.makeArray(t),s=i.length;while(s--)r=i[s],(r.selected=x.inArray(x(r).val(),o)>=0)&&(n=!0);return n||(e.selectedIndex=-1),o}}},attr:function(e,t,n){var i,o,s=e.nodeType;if(e&&3!==s&&8!==s&&2!==s)return typeof e.getAttribute===r?x.prop(e,t,n):(1===s&&x.isXMLDoc(e)||(t=t.toLowerCase(),i=x.attrHooks[t]||(x.expr.match.bool.test(t)?M:R)),n===undefined?i&&"get"in i&&null!==(o=i.get(e,t))?o:(o=x.find.attr(e,t),null==o?undefined:o):null!==n?i&&"set"in i&&(o=i.set(e,n,t))!==undefined?o:(e.setAttribute(t,n+""),n):(x.removeAttr(e,t),undefined))},removeAttr:function(e,t){var n,r,i=0,o=t&&t.match(w);if(o&&1===e.nodeType)while(n=o[i++])r=x.propFix[n]||n,x.expr.match.bool.test(n)&&(e[r]=!1),e.removeAttribute(n)},attrHooks:{type:{set:function(e,t){if(!x.support.radioValue&&"radio"===t&&x.nodeName(e,"input")){var n=e.value;return e.setAttribute("type",t),n&&(e.value=n),t}}}},propFix:{"for":"htmlFor","class":"className"},prop:function(e,t,n){var r,i,o,s=e.nodeType;if(e&&3!==s&&8!==s&&2!==s)return o=1!==s||!x.isXMLDoc(e),o&&(t=x.propFix[t]||t,i=x.propHooks[t]),n!==undefined?i&&"set"in i&&(r=i.set(e,n,t))!==undefined?r:e[t]=n:i&&"get"in i&&null!==(r=i.get(e,t))?r:e[t]},propHooks:{tabIndex:{get:function(e){return e.hasAttribute("tabindex")||B.test(e.nodeName)||e.href?e.tabIndex:-1}}}}),M={set:function(e,t,n){return t===!1?x.removeAttr(e,n):e.setAttribute(n,n),n}},x.each(x.expr.match.bool.source.match(/\w+/g),function(e,t){var n=x.expr.attrHandle[t]||x.find.attr;x.expr.attrHandle[t]=function(e,t,r){var i=x.expr.attrHandle[t],o=r?undefined:(x.expr.attrHandle[t]=undefined)!=n(e,t,r)?t.toLowerCase():null;return x.expr.attrHandle[t]=i,o}}),x.support.optSelected||(x.propHooks.selected={get:function(e){var t=e.parentNode;return t&&t.parentNode&&t.parentNode.selectedIndex,null}}),x.each(["tabIndex","readOnly","maxLength","cellSpacing","cellPadding","rowSpan","colSpan","useMap","frameBorder","contentEditable"],function(){x.propFix[this.toLowerCase()]=this}),x.each(["radio","checkbox"],function(){x.valHooks[this]={set:function(e,t){return x.isArray(t)?e.checked=x.inArray(x(e).val(),t)>=0:undefined}},x.support.checkOn||(x.valHooks[this].get=function(e){return null===e.getAttribute("value")?"on":e.value})});var I=/^key/,z=/^(?:mouse|contextmenu)|click/,_=/^(?:focusinfocus|focusoutblur)$/,X=/^([^.]*)(?:\.(.+)|)$/;function U(){return!0}function Y(){return!1}function V(){try{return o.activeElement}catch(e){}}x.event={global:{},add:function(e,t,n,i,o){var s,a,u,l,c,p,f,h,d,g,m,y=q.get(e);if(y){n.handler&&(s=n,n=s.handler,o=s.selector),n.guid||(n.guid=x.guid++),(l=y.events)||(l=y.events={}),(a=y.handle)||(a=y.handle=function(e){return typeof x===r||e&&x.event.triggered===e.type?undefined:x.event.dispatch.apply(a.elem,arguments)},a.elem=e),t=(t||"").match(w)||[""],c=t.length;while(c--)u=X.exec(t[c])||[],d=m=u[1],g=(u[2]||"").split(".").sort(),d&&(f=x.event.special[d]||{},d=(o?f.delegateType:f.bindType)||d,f=x.event.special[d]||{},p=x.extend({type:d,origType:m,data:i,handler:n,guid:n.guid,selector:o,needsContext:o&&x.expr.match.needsContext.test(o),namespace:g.join(".")},s),(h=l[d])||(h=l[d]=[],h.delegateCount=0,f.setup&&f.setup.call(e,i,g,a)!==!1||e.addEventListener&&e.addEventListener(d,a,!1)),f.add&&(f.add.call(e,p),p.handler.guid||(p.handler.guid=n.guid)),o?h.splice(h.delegateCount++,0,p):h.push(p),x.event.global[d]=!0);e=null}},remove:function(e,t,n,r,i){var o,s,a,u,l,c,p,f,h,d,g,m=q.hasData(e)&&q.get(e);if(m&&(u=m.events)){t=(t||"").match(w)||[""],l=t.length;while(l--)if(a=X.exec(t[l])||[],h=g=a[1],d=(a[2]||"").split(".").sort(),h){p=x.event.special[h]||{},h=(r?p.delegateType:p.bindType)||h,f=u[h]||[],a=a[2]&&RegExp("(^|\\.)"+d.join("\\.(?:.*\\.|)")+"(\\.|$)"),s=o=f.length;while(o--)c=f[o],!i&&g!==c.origType||n&&n.guid!==c.guid||a&&!a.test(c.namespace)||r&&r!==c.selector&&("**"!==r||!c.selector)||(f.splice(o,1),c.selector&&f.delegateCount--,p.remove&&p.remove.call(e,c));s&&!f.length&&(p.teardown&&p.teardown.call(e,d,m.handle)!==!1||x.removeEvent(e,h,m.handle),delete u[h])}else for(h in u)x.event.remove(e,h+t[l],n,r,!0);x.isEmptyObject(u)&&(delete m.handle,q.remove(e,"events"))}},trigger:function(t,n,r,i){var s,a,u,l,c,p,f,h=[r||o],d=y.call(t,"type")?t.type:t,g=y.call(t,"namespace")?t.namespace.split("."):[];if(a=u=r=r||o,3!==r.nodeType&&8!==r.nodeType&&!_.test(d+x.event.triggered)&&(d.indexOf(".")>=0&&(g=d.split("."),d=g.shift(),g.sort()),c=0>d.indexOf(":")&&"on"+d,t=t[x.expando]?t:new x.Event(d,"object"==typeof t&&t),t.isTrigger=i?2:3,t.namespace=g.join("."),t.namespace_re=t.namespace?RegExp("(^|\\.)"+g.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,t.result=undefined,t.target||(t.target=r),n=null==n?[t]:x.makeArray(n,[t]),f=x.event.special[d]||{},i||!f.trigger||f.trigger.apply(r,n)!==!1)){if(!i&&!f.noBubble&&!x.isWindow(r)){for(l=f.delegateType||d,_.test(l+d)||(a=a.parentNode);a;a=a.parentNode)h.push(a),u=a;u===(r.ownerDocument||o)&&h.push(u.defaultView||u.parentWindow||e)}s=0;while((a=h[s++])&&!t.isPropagationStopped())t.type=s>1?l:f.bindType||d,p=(q.get(a,"events")||{})[t.type]&&q.get(a,"handle"),p&&p.apply(a,n),p=c&&a[c],p&&x.acceptData(a)&&p.apply&&p.apply(a,n)===!1&&t.preventDefault();return t.type=d,i||t.isDefaultPrevented()||f._default&&f._default.apply(h.pop(),n)!==!1||!x.acceptData(r)||c&&x.isFunction(r[d])&&!x.isWindow(r)&&(u=r[c],u&&(r[c]=null),x.event.triggered=d,r[d](),x.event.triggered=undefined,u&&(r[c]=u)),t.result}},dispatch:function(e){e=x.event.fix(e);var t,n,r,i,o,s=[],a=d.call(arguments),u=(q.get(this,"events")||{})[e.type]||[],l=x.event.special[e.type]||{};if(a[0]=e,e.delegateTarget=this,!l.preDispatch||l.preDispatch.call(this,e)!==!1){s=x.event.handlers.call(this,e,u),t=0;while((i=s[t++])&&!e.isPropagationStopped()){e.currentTarget=i.elem,n=0;while((o=i.handlers[n++])&&!e.isImmediatePropagationStopped())(!e.namespace_re||e.namespace_re.test(o.namespace))&&(e.handleObj=o,e.data=o.data,r=((x.event.special[o.origType]||{}).handle||o.handler).apply(i.elem,a),r!==undefined&&(e.result=r)===!1&&(e.preventDefault(),e.stopPropagation()))}return l.postDispatch&&l.postDispatch.call(this,e),e.result}},handlers:function(e,t){var n,r,i,o,s=[],a=t.delegateCount,u=e.target;if(a&&u.nodeType&&(!e.button||"click"!==e.type))for(;u!==this;u=u.parentNode||this)if(u.disabled!==!0||"click"!==e.type){for(r=[],n=0;a>n;n++)o=t[n],i=o.selector+" ",r[i]===undefined&&(r[i]=o.needsContext?x(i,this).index(u)>=0:x.find(i,this,null,[u]).length),r[i]&&r.push(o);r.length&&s.push({elem:u,handlers:r})}return t.length>a&&s.push({elem:this,handlers:t.slice(a)}),s},props:"altKey bubbles cancelable ctrlKey currentTarget eventPhase metaKey relatedTarget shiftKey target timeStamp view which".split(" "),fixHooks:{},keyHooks:{props:"char charCode key keyCode".split(" "),filter:function(e,t){return null==e.which&&(e.which=null!=t.charCode?t.charCode:t.keyCode),e}},mouseHooks:{props:"button buttons clientX clientY offsetX offsetY pageX pageY screenX screenY toElement".split(" "),filter:function(e,t){var n,r,i,s=t.button;return null==e.pageX&&null!=t.clientX&&(n=e.target.ownerDocument||o,r=n.documentElement,i=n.body,e.pageX=t.clientX+(r&&r.scrollLeft||i&&i.scrollLeft||0)-(r&&r.clientLeft||i&&i.clientLeft||0),e.pageY=t.clientY+(r&&r.scrollTop||i&&i.scrollTop||0)-(r&&r.clientTop||i&&i.clientTop||0)),e.which||s===undefined||(e.which=1&s?1:2&s?3:4&s?2:0),e}},fix:function(e){if(e[x.expando])return e;var t,n,r,i=e.type,s=e,a=this.fixHooks[i];a||(this.fixHooks[i]=a=z.test(i)?this.mouseHooks:I.test(i)?this.keyHooks:{}),r=a.props?this.props.concat(a.props):this.props,e=new x.Event(s),t=r.length;while(t--)n=r[t],e[n]=s[n];return e.target||(e.target=o),3===e.target.nodeType&&(e.target=e.target.parentNode),a.filter?a.filter(e,s):e},special:{load:{noBubble:!0},focus:{trigger:function(){return this!==V()&&this.focus?(this.focus(),!1):undefined},delegateType:"focusin"},blur:{trigger:function(){return this===V()&&this.blur?(this.blur(),!1):undefined},delegateType:"focusout"},click:{trigger:function(){return"checkbox"===this.type&&this.click&&x.nodeName(this,"input")?(this.click(),!1):undefined},_default:function(e){return x.nodeName(e.target,"a")}},beforeunload:{postDispatch:function(e){e.result!==undefined&&(e.originalEvent.returnValue=e.result)}}},simulate:function(e,t,n,r){var i=x.extend(new x.Event,n,{type:e,isSimulated:!0,originalEvent:{}});r?x.event.trigger(i,null,t):x.event.dispatch.call(t,i),i.isDefaultPrevented()&&n.preventDefault()}},x.removeEvent=function(e,t,n){e.removeEventListener&&e.removeEventListener(t,n,!1)},x.Event=function(e,t){return this instanceof x.Event?(e&&e.type?(this.originalEvent=e,this.type=e.type,this.isDefaultPrevented=e.defaultPrevented||e.getPreventDefault&&e.getPreventDefault()?U:Y):this.type=e,t&&x.extend(this,t),this.timeStamp=e&&e.timeStamp||x.now(),this[x.expando]=!0,undefined):new x.Event(e,t)},x.Event.prototype={isDefaultPrevented:Y,isPropagationStopped:Y,isImmediatePropagationStopped:Y,preventDefault:function(){var e=this.originalEvent;this.isDefaultPrevented=U,e&&e.preventDefault&&e.preventDefault()},stopPropagation:function(){var e=this.originalEvent;this.isPropagationStopped=U,e&&e.stopPropagation&&e.stopPropagation()},stopImmediatePropagation:function(){this.isImmediatePropagationStopped=U,this.stopPropagation()}},x.each({mouseenter:"mouseover",mouseleave:"mouseout"},function(e,t){x.event.special[e]={delegateType:t,bindType:t,handle:function(e){var n,r=this,i=e.relatedTarget,o=e.handleObj;return(!i||i!==r&&!x.contains(r,i))&&(e.type=o.origType,n=o.handler.apply(this,arguments),e.type=t),n}}}),x.support.focusinBubbles||x.each({focus:"focusin",blur:"focusout"},function(e,t){var n=0,r=function(e){x.event.simulate(t,e.target,x.event.fix(e),!0)};x.event.special[t]={setup:function(){0===n++&&o.addEventListener(e,r,!0)},teardown:function(){0===--n&&o.removeEventListener(e,r,!0)}}}),x.fn.extend({on:function(e,t,n,r,i){var o,s;if("object"==typeof e){"string"!=typeof t&&(n=n||t,t=undefined);for(s in e)this.on(s,t,n,e[s],i);return this}if(null==n&&null==r?(r=t,n=t=undefined):null==r&&("string"==typeof t?(r=n,n=undefined):(r=n,n=t,t=undefined)),r===!1)r=Y;else if(!r)return this;return 1===i&&(o=r,r=function(e){return x().off(e),o.apply(this,arguments)},r.guid=o.guid||(o.guid=x.guid++)),this.each(function(){x.event.add(this,e,r,n,t)})},one:function(e,t,n,r){return this.on(e,t,n,r,1)},off:function(e,t,n){var r,i;if(e&&e.preventDefault&&e.handleObj)return r=e.handleObj,x(e.delegateTarget).off(r.namespace?r.origType+"."+r.namespace:r.origType,r.selector,r.handler),this;if("object"==typeof e){for(i in e)this.off(i,t,e[i]);return this}return(t===!1||"function"==typeof t)&&(n=t,t=undefined),n===!1&&(n=Y),this.each(function(){x.event.remove(this,e,n,t)})},trigger:function(e,t){return this.each(function(){x.event.trigger(e,t,this)})},triggerHandler:function(e,t){var n=this[0];return n?x.event.trigger(e,t,n,!0):undefined}});var G=/^.[^:#\[\.,]*$/,J=/^(?:parents|prev(?:Until|All))/,Q=x.expr.match.needsContext,K={children:!0,contents:!0,next:!0,prev:!0};x.fn.extend({find:function(e){var t,n=[],r=this,i=r.length;if("string"!=typeof e)return this.pushStack(x(e).filter(function(){for(t=0;i>t;t++)if(x.contains(r[t],this))return!0}));for(t=0;i>t;t++)x.find(e,r[t],n);return n=this.pushStack(i>1?x.unique(n):n),n.selector=this.selector?this.selector+" "+e:e,n},has:function(e){var t=x(e,this),n=t.length;return this.filter(function(){var e=0;for(;n>e;e++)if(x.contains(this,t[e]))return!0})},not:function(e){return this.pushStack(et(this,e||[],!0))},filter:function(e){return this.pushStack(et(this,e||[],!1))},is:function(e){return!!et(this,"string"==typeof e&&Q.test(e)?x(e):e||[],!1).length},closest:function(e,t){var n,r=0,i=this.length,o=[],s=Q.test(e)||"string"!=typeof e?x(e,t||this.context):0;for(;i>r;r++)for(n=this[r];n&&n!==t;n=n.parentNode)if(11>n.nodeType&&(s?s.index(n)>-1:1===n.nodeType&&x.find.matchesSelector(n,e))){n=o.push(n);break}return this.pushStack(o.length>1?x.unique(o):o)},index:function(e){return e?"string"==typeof e?g.call(x(e),this[0]):g.call(this,e.jquery?e[0]:e):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(e,t){var n="string"==typeof e?x(e,t):x.makeArray(e&&e.nodeType?[e]:e),r=x.merge(this.get(),n);return this.pushStack(x.unique(r))},addBack:function(e){return this.add(null==e?this.prevObject:this.prevObject.filter(e))}});function Z(e,t){while((e=e[t])&&1!==e.nodeType);return e}x.each({parent:function(e){var t=e.parentNode;return t&&11!==t.nodeType?t:null},parents:function(e){return x.dir(e,"parentNode")},parentsUntil:function(e,t,n){return x.dir(e,"parentNode",n)},next:function(e){return Z(e,"nextSibling")},prev:function(e){return Z(e,"previousSibling")},nextAll:function(e){return x.dir(e,"nextSibling")},prevAll:function(e){return x.dir(e,"previousSibling")},nextUntil:function(e,t,n){return x.dir(e,"nextSibling",n)},prevUntil:function(e,t,n){return x.dir(e,"previousSibling",n)},siblings:function(e){return x.sibling((e.parentNode||{}).firstChild,e)},children:function(e){return x.sibling(e.firstChild)},contents:function(e){return e.contentDocument||x.merge([],e.childNodes)}},function(e,t){x.fn[e]=function(n,r){var i=x.map(this,t,n);return"Until"!==e.slice(-5)&&(r=n),r&&"string"==typeof r&&(i=x.filter(r,i)),this.length>1&&(K[e]||x.unique(i),J.test(e)&&i.reverse()),this.pushStack(i)}}),x.extend({filter:function(e,t,n){var r=t[0];return n&&(e=":not("+e+")"),1===t.length&&1===r.nodeType?x.find.matchesSelector(r,e)?[r]:[]:x.find.matches(e,x.grep(t,function(e){return 1===e.nodeType}))},dir:function(e,t,n){var r=[],i=n!==undefined;while((e=e[t])&&9!==e.nodeType)if(1===e.nodeType){if(i&&x(e).is(n))break;r.push(e)}return r},sibling:function(e,t){var n=[];for(;e;e=e.nextSibling)1===e.nodeType&&e!==t&&n.push(e);return n}});function et(e,t,n){if(x.isFunction(t))return x.grep(e,function(e,r){return!!t.call(e,r,e)!==n});if(t.nodeType)return x.grep(e,function(e){return e===t!==n});if("string"==typeof t){if(G.test(t))return x.filter(t,e,n);t=x.filter(t,e)}return x.grep(e,function(e){return g.call(t,e)>=0!==n})}var tt=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi,nt=/<([\w:]+)/,rt=/<|&#?\w+;/,it=/<(?:script|style|link)/i,ot=/^(?:checkbox|radio)$/i,st=/checked\s*(?:[^=]|=\s*.checked.)/i,at=/^$|\/(?:java|ecma)script/i,ut=/^true\/(.*)/,lt=/^\s*\s*$/g,ct={option:[1,""],thead:[1,"","
          "],col:[2,"","
          "],tr:[2,"","
          "],td:[3,"","
          "],_default:[0,"",""]};ct.optgroup=ct.option,ct.tbody=ct.tfoot=ct.colgroup=ct.caption=ct.thead,ct.th=ct.td,x.fn.extend({text:function(e){return x.access(this,function(e){return e===undefined?x.text(this):this.empty().append((this[0]&&this[0].ownerDocument||o).createTextNode(e))},null,e,arguments.length)},append:function(){return this.domManip(arguments,function(e){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var t=pt(this,e);t.appendChild(e)}})},prepend:function(){return this.domManip(arguments,function(e){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var t=pt(this,e);t.insertBefore(e,t.firstChild)}})},before:function(){return this.domManip(arguments,function(e){this.parentNode&&this.parentNode.insertBefore(e,this)})},after:function(){return this.domManip(arguments,function(e){this.parentNode&&this.parentNode.insertBefore(e,this.nextSibling)})},remove:function(e,t){var n,r=e?x.filter(e,this):this,i=0;for(;null!=(n=r[i]);i++)t||1!==n.nodeType||x.cleanData(mt(n)),n.parentNode&&(t&&x.contains(n.ownerDocument,n)&&dt(mt(n,"script")),n.parentNode.removeChild(n));return this},empty:function(){var e,t=0;for(;null!=(e=this[t]);t++)1===e.nodeType&&(x.cleanData(mt(e,!1)),e.textContent="");return this},clone:function(e,t){return e=null==e?!1:e,t=null==t?e:t,this.map(function(){return x.clone(this,e,t)})},html:function(e){return x.access(this,function(e){var t=this[0]||{},n=0,r=this.length;if(e===undefined&&1===t.nodeType)return t.innerHTML;if("string"==typeof e&&!it.test(e)&&!ct[(nt.exec(e)||["",""])[1].toLowerCase()]){e=e.replace(tt,"<$1>");try{for(;r>n;n++)t=this[n]||{},1===t.nodeType&&(x.cleanData(mt(t,!1)),t.innerHTML=e);t=0}catch(i){}}t&&this.empty().append(e)},null,e,arguments.length)},replaceWith:function(){var e=x.map(this,function(e){return[e.nextSibling,e.parentNode]}),t=0;return this.domManip(arguments,function(n){var r=e[t++],i=e[t++];i&&(r&&r.parentNode!==i&&(r=this.nextSibling),x(this).remove(),i.insertBefore(n,r))},!0),t?this:this.remove()},detach:function(e){return this.remove(e,!0)},domManip:function(e,t,n){e=f.apply([],e);var r,i,o,s,a,u,l=0,c=this.length,p=this,h=c-1,d=e[0],g=x.isFunction(d);if(g||!(1>=c||"string"!=typeof d||x.support.checkClone)&&st.test(d))return this.each(function(r){var i=p.eq(r);g&&(e[0]=d.call(this,r,i.html())),i.domManip(e,t,n)});if(c&&(r=x.buildFragment(e,this[0].ownerDocument,!1,!n&&this),i=r.firstChild,1===r.childNodes.length&&(r=i),i)){for(o=x.map(mt(r,"script"),ft),s=o.length;c>l;l++)a=r,l!==h&&(a=x.clone(a,!0,!0),s&&x.merge(o,mt(a,"script"))),t.call(this[l],a,l);if(s)for(u=o[o.length-1].ownerDocument,x.map(o,ht),l=0;s>l;l++)a=o[l],at.test(a.type||"")&&!q.access(a,"globalEval")&&x.contains(u,a)&&(a.src?x._evalUrl(a.src):x.globalEval(a.textContent.replace(lt,"")))}return this}}),x.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(e,t){x.fn[e]=function(e){var n,r=[],i=x(e),o=i.length-1,s=0;for(;o>=s;s++)n=s===o?this:this.clone(!0),x(i[s])[t](n),h.apply(r,n.get());return this.pushStack(r)}}),x.extend({clone:function(e,t,n){var r,i,o,s,a=e.cloneNode(!0),u=x.contains(e.ownerDocument,e);if(!(x.support.noCloneChecked||1!==e.nodeType&&11!==e.nodeType||x.isXMLDoc(e)))for(s=mt(a),o=mt(e),r=0,i=o.length;i>r;r++)yt(o[r],s[r]);if(t)if(n)for(o=o||mt(e),s=s||mt(a),r=0,i=o.length;i>r;r++)gt(o[r],s[r]);else gt(e,a);return s=mt(a,"script"),s.length>0&&dt(s,!u&&mt(e,"script")),a},buildFragment:function(e,t,n,r){var i,o,s,a,u,l,c=0,p=e.length,f=t.createDocumentFragment(),h=[];for(;p>c;c++)if(i=e[c],i||0===i)if("object"===x.type(i))x.merge(h,i.nodeType?[i]:i);else if(rt.test(i)){o=o||f.appendChild(t.createElement("div")),s=(nt.exec(i)||["",""])[1].toLowerCase(),a=ct[s]||ct._default,o.innerHTML=a[1]+i.replace(tt,"<$1>")+a[2],l=a[0];while(l--)o=o.lastChild;x.merge(h,o.childNodes),o=f.firstChild,o.textContent=""}else h.push(t.createTextNode(i));f.textContent="",c=0;while(i=h[c++])if((!r||-1===x.inArray(i,r))&&(u=x.contains(i.ownerDocument,i),o=mt(f.appendChild(i),"script"),u&&dt(o),n)){l=0;while(i=o[l++])at.test(i.type||"")&&n.push(i)}return f},cleanData:function(e){var t,n,r,i,o,s,a=x.event.special,u=0;for(;(n=e[u])!==undefined;u++){if(F.accepts(n)&&(o=n[q.expando],o&&(t=q.cache[o]))){if(r=Object.keys(t.events||{}),r.length)for(s=0;(i=r[s])!==undefined;s++)a[i]?x.event.remove(n,i):x.removeEvent(n,i,t.handle);q.cache[o]&&delete q.cache[o]}delete L.cache[n[L.expando]]}},_evalUrl:function(e){return x.ajax({url:e,type:"GET",dataType:"script",async:!1,global:!1,"throws":!0})}});function pt(e,t){return x.nodeName(e,"table")&&x.nodeName(1===t.nodeType?t:t.firstChild,"tr")?e.getElementsByTagName("tbody")[0]||e.appendChild(e.ownerDocument.createElement("tbody")):e}function ft(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function ht(e){var t=ut.exec(e.type);return t?e.type=t[1]:e.removeAttribute("type"),e}function dt(e,t){var n=e.length,r=0;for(;n>r;r++)q.set(e[r],"globalEval",!t||q.get(t[r],"globalEval"))}function gt(e,t){var n,r,i,o,s,a,u,l;if(1===t.nodeType){if(q.hasData(e)&&(o=q.access(e),s=q.set(t,o),l=o.events)){delete s.handle,s.events={};for(i in l)for(n=0,r=l[i].length;r>n;n++)x.event.add(t,i,l[i][n])}L.hasData(e)&&(a=L.access(e),u=x.extend({},a),L.set(t,u))}}function mt(e,t){var n=e.getElementsByTagName?e.getElementsByTagName(t||"*"):e.querySelectorAll?e.querySelectorAll(t||"*"):[];return t===undefined||t&&x.nodeName(e,t)?x.merge([e],n):n}function yt(e,t){var n=t.nodeName.toLowerCase();"input"===n&&ot.test(e.type)?t.checked=e.checked:("input"===n||"textarea"===n)&&(t.defaultValue=e.defaultValue)}x.fn.extend({wrapAll:function(e){var t;return x.isFunction(e)?this.each(function(t){x(this).wrapAll(e.call(this,t))}):(this[0]&&(t=x(e,this[0].ownerDocument).eq(0).clone(!0),this[0].parentNode&&t.insertBefore(this[0]),t.map(function(){var e=this;while(e.firstElementChild)e=e.firstElementChild;return e}).append(this)),this)},wrapInner:function(e){return x.isFunction(e)?this.each(function(t){x(this).wrapInner(e.call(this,t))}):this.each(function(){var t=x(this),n=t.contents();n.length?n.wrapAll(e):t.append(e)})},wrap:function(e){var t=x.isFunction(e);return this.each(function(n){x(this).wrapAll(t?e.call(this,n):e)})},unwrap:function(){return this.parent().each(function(){x.nodeName(this,"body")||x(this).replaceWith(this.childNodes)}).end()}});var vt,xt,bt=/^(none|table(?!-c[ea]).+)/,wt=/^margin/,Tt=RegExp("^("+b+")(.*)$","i"),Ct=RegExp("^("+b+")(?!px)[a-z%]+$","i"),kt=RegExp("^([+-])=("+b+")","i"),Nt={BODY:"block"},Et={position:"absolute",visibility:"hidden",display:"block"},St={letterSpacing:0,fontWeight:400},jt=["Top","Right","Bottom","Left"],Dt=["Webkit","O","Moz","ms"];function At(e,t){if(t in e)return t;var n=t.charAt(0).toUpperCase()+t.slice(1),r=t,i=Dt.length;while(i--)if(t=Dt[i]+n,t in e)return t;return r}function Lt(e,t){return e=t||e,"none"===x.css(e,"display")||!x.contains(e.ownerDocument,e)}function qt(t){return e.getComputedStyle(t,null)}function Ht(e,t){var n,r,i,o=[],s=0,a=e.length;for(;a>s;s++)r=e[s],r.style&&(o[s]=q.get(r,"olddisplay"),n=r.style.display,t?(o[s]||"none"!==n||(r.style.display=""),""===r.style.display&&Lt(r)&&(o[s]=q.access(r,"olddisplay",Rt(r.nodeName)))):o[s]||(i=Lt(r),(n&&"none"!==n||!i)&&q.set(r,"olddisplay",i?n:x.css(r,"display"))));for(s=0;a>s;s++)r=e[s],r.style&&(t&&"none"!==r.style.display&&""!==r.style.display||(r.style.display=t?o[s]||"":"none"));return e}x.fn.extend({css:function(e,t){return x.access(this,function(e,t,n){var r,i,o={},s=0;if(x.isArray(t)){for(r=qt(e),i=t.length;i>s;s++)o[t[s]]=x.css(e,t[s],!1,r);return o}return n!==undefined?x.style(e,t,n):x.css(e,t)},e,t,arguments.length>1)},show:function(){return Ht(this,!0)},hide:function(){return Ht(this)},toggle:function(e){return"boolean"==typeof e?e?this.show():this.hide():this.each(function(){Lt(this)?x(this).show():x(this).hide()})}}),x.extend({cssHooks:{opacity:{get:function(e,t){if(t){var n=vt(e,"opacity");return""===n?"1":n}}}},cssNumber:{columnCount:!0,fillOpacity:!0,fontWeight:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,widows:!0,zIndex:!0,zoom:!0},cssProps:{"float":"cssFloat"},style:function(e,t,n,r){if(e&&3!==e.nodeType&&8!==e.nodeType&&e.style){var i,o,s,a=x.camelCase(t),u=e.style;return t=x.cssProps[a]||(x.cssProps[a]=At(u,a)),s=x.cssHooks[t]||x.cssHooks[a],n===undefined?s&&"get"in s&&(i=s.get(e,!1,r))!==undefined?i:u[t]:(o=typeof n,"string"===o&&(i=kt.exec(n))&&(n=(i[1]+1)*i[2]+parseFloat(x.css(e,t)),o="number"),null==n||"number"===o&&isNaN(n)||("number"!==o||x.cssNumber[a]||(n+="px"),x.support.clearCloneStyle||""!==n||0!==t.indexOf("background")||(u[t]="inherit"),s&&"set"in s&&(n=s.set(e,n,r))===undefined||(u[t]=n)),undefined)}},css:function(e,t,n,r){var i,o,s,a=x.camelCase(t);return t=x.cssProps[a]||(x.cssProps[a]=At(e.style,a)),s=x.cssHooks[t]||x.cssHooks[a],s&&"get"in s&&(i=s.get(e,!0,n)),i===undefined&&(i=vt(e,t,r)),"normal"===i&&t in St&&(i=St[t]),""===n||n?(o=parseFloat(i),n===!0||x.isNumeric(o)?o||0:i):i}}),vt=function(e,t,n){var r,i,o,s=n||qt(e),a=s?s.getPropertyValue(t)||s[t]:undefined,u=e.style;return s&&(""!==a||x.contains(e.ownerDocument,e)||(a=x.style(e,t)),Ct.test(a)&&wt.test(t)&&(r=u.width,i=u.minWidth,o=u.maxWidth,u.minWidth=u.maxWidth=u.width=a,a=s.width,u.width=r,u.minWidth=i,u.maxWidth=o)),a};function Ot(e,t,n){var r=Tt.exec(t);return r?Math.max(0,r[1]-(n||0))+(r[2]||"px"):t}function Ft(e,t,n,r,i){var o=n===(r?"border":"content")?4:"width"===t?1:0,s=0;for(;4>o;o+=2)"margin"===n&&(s+=x.css(e,n+jt[o],!0,i)),r?("content"===n&&(s-=x.css(e,"padding"+jt[o],!0,i)),"margin"!==n&&(s-=x.css(e,"border"+jt[o]+"Width",!0,i))):(s+=x.css(e,"padding"+jt[o],!0,i),"padding"!==n&&(s+=x.css(e,"border"+jt[o]+"Width",!0,i)));return s}function Pt(e,t,n){var r=!0,i="width"===t?e.offsetWidth:e.offsetHeight,o=qt(e),s=x.support.boxSizing&&"border-box"===x.css(e,"boxSizing",!1,o);if(0>=i||null==i){if(i=vt(e,t,o),(0>i||null==i)&&(i=e.style[t]),Ct.test(i))return i;r=s&&(x.support.boxSizingReliable||i===e.style[t]),i=parseFloat(i)||0}return i+Ft(e,t,n||(s?"border":"content"),r,o)+"px"}function Rt(e){var t=o,n=Nt[e];return n||(n=Mt(e,t),"none"!==n&&n||(xt=(xt||x(" + +

          - - File not found + + {{ 'file.view.not_found' | translate }}

          diff --git a/docs-web/src/main/webapp/src/partial/docs/group.profile.html b/docs-web/src/main/webapp/src/partial/docs/group.profile.html index 8aa8c586..799ac65f 100644 --- a/docs-web/src/main/webapp/src/partial/docs/group.profile.html +++ b/docs-web/src/main/webapp/src/partial/docs/group.profile.html @@ -2,20 +2,21 @@

          {{ group.name }}

          -

          Members

          +

          {{ 'group.profile.members' | translate }}

          • {{ member }}
          • -
          • No member
          • +
          • {{ 'group.profile.no_members' | translate }}
          -

          Related links

          +

          {{ 'group.profile.related_links' | translate }}

          diff --git a/docs-web/src/main/webapp/src/partial/docs/import.html b/docs-web/src/main/webapp/src/partial/docs/import.html new file mode 100644 index 00000000..dd215a81 --- /dev/null +++ b/docs-web/src/main/webapp/src/partial/docs/import.html @@ -0,0 +1,10 @@ +
          + + +
          \ No newline at end of file diff --git a/docs-web/src/main/webapp/src/partial/docs/login.html b/docs-web/src/main/webapp/src/partial/docs/login.html index 725c3238..822ec9a1 100644 --- a/docs-web/src/main/webapp/src/partial/docs/login.html +++ b/docs-web/src/main/webapp/src/partial/docs/login.html @@ -3,44 +3,86 @@ .navbar { display: none; } + + /* Absolute positionned footer on top of the login background */ + .footer { + position: absolute; + bottom: 0; + left: 0; + right: 0; + border: none; + } + + @media screen and (min-width: 768px) { + .footer { + width: 50%; + } + } + + /* Smaller links everywhere on login */ + a { + font-size: 90%; + color: #666; + } -